Merge lp:~mhall119/loco-team-portal/json-services into lp:loco-team-portal

Proposed by Michael Hall
Status: Merged
Merged at revision: 154
Proposed branch: lp:~mhall119/loco-team-portal/json-services
Merge into: lp:loco-team-portal
Diff against target: 215 lines (+189/-0)
4 files modified
loco_directory/services/__init__.py (+134/-0)
loco_directory/services/urls.py (+20/-0)
loco_directory/services/views.py (+34/-0)
loco_directory/urls.py (+1/-0)
To merge this branch: bzr merge lp:~mhall119/loco-team-portal/json-services
Reviewer Review Type Date Requested Status
Daniel Holbach (community) Approve
Efrain Valles Needs Fixing
Review via email: mp+26697@code.launchpad.net

This proposal supersedes a proposal from 2010-06-03.

Description of the change

Provides read-only REST/JSON services for Django models used by LoCo Directory.

To post a comment you must log in.
Revision history for this message
Daniel Holbach (dholbach) wrote : Posted in a previous version of this proposal

Wow you put a lot of work into this.

 - It might be worth noting that parts of the code are from http://bitbucket.org/oct09/experimental/src/739a074f7ea2/utils.py for reference.
 - Is @author read or used by anything?
 - Would it be worth displaying just a list of team names and team ids for /services/teams? Why would anyone use /services/teams/<x>? :)
 - is /services/teams/1 usable enough? I thought people would be after /services/teams/ubuntu-fr?
 - what is "/services/comments/" about?

This needs a code review from a few more people. I'm not sure I can do the code fully justice. I feel slightly uneasy about relying on certain strings in field names like "__", "pk", etc, etc.

review: Needs Information
Revision history for this message
Michael Hall (mhall119) wrote :

I removed the functions in common.utils, which it turns out I didn't need because I'm not offering write capabilities (yet). That takes care of #1 above, as well as relying on "pk" and "__". I also removed the @author comments, those were just auto-generated by Eclipse when I created the new files.

/services/teams/1 might be usable to someone, but it cost me nothing to allow it, so I don't see any reason to take it out. Since this is an API, I don't think we really need a user-friendly URL scheme like /services/teams/ubuntu-fr, however you can use the LP name in a search: /services/teams/?lp_name=ubuntu-fl

/services/comments/ is the API for accessing comments on team events.

143. By Daniel Holbach

update translations

144. By Daniel Holbach

released 0.2.9

145. By Michael Hall

Check that user is authenticated (not AnonymousUser) before checking for group membership. (LP: #589226)

146. By Michael Hall

Add break that will clear the floating property of elements after the Attending Team Events list

147. By Daniel Holbach

update translation template

148. By Daniel Holbach

released 0.2.10

149. By Thomas Bechtold

merged lp:~dholbach/loco-directory/579842

Revision history for this message
Efrain Valles (effie-jayx) wrote :

I agree with mhall119, urls don't need to be that friendly considering a ?lp_name can be used to fetch for a team with a LP name. I do beleive we should include some credits for the code borrowed.

review: Approve
150. By Daniel Holbach

apply patch from Seung Soo, Ha

151. By Daniel Holbach

update template

152. By Michael Hall <mhall@mhall-laptop>

Merge with trunk and updated copyright information in services

Revision history for this message
Michael Hall (mhall119) wrote :

dholbach: Is there something you're waiting on for this branch to be merged?

Revision history for this message
Dave Walker (davewalker) wrote :

Looking at the diff, I have some concerns about the Licence. http://bitbucket.org/oct09/main/src/tip/COPYING allows derivation, but does insist that the licence text is provided aswell. I don't know if linking to their licence is enough. IANAL ofc.

Revision history for this message
Daniel Holbach (dholbach) wrote :

pyflakes output on the new code:
 - loco_directory/services/__init__.py:10: 'utils' imported but unused
 - (indent too much): loco_directory/services/views.py:34: invalid syntax

     ^
 - loco_directory/common/utils.py:3: 'django' imported but unused
 - loco_directory/common/utils.py:7: redefinition of unused 'django' from line 3
 - loco_directory/common/utils.py:11: redefinition of unused 'red' from line 9

pylint (services/__init__.py)
 - W: 15:model_service: Unused variable 'rem_url'
 - W: 26:model_entity: Unused variable 'rem_url'
 - W: 46:model_collection: Unused argument 'url'
 - W: 72:get_model_id: Unused argument 'request'
 - W: 87:decode: Unused argument 'url'
 - W: 87:decode: Unused argument 'klass'
 - W: 87:decode: Unused argument 'entity'
 - W:104:JSONEncoder.get_field_value: Redefining built-in 'object'

Questions:
 - Is decode() ever used?
 - Is there a reason why most of the code is in __init__.py?

Revision history for this message
Daniel Holbach (dholbach) wrote :

Thanks a lot for your work on this.

153. By Michael Hall <mhall@mhall-laptop>

Removed unneeded imports

Revision history for this message
Daniel Holbach (dholbach) wrote :

Do you think you can at least fix these two?

loco_directory/services/views.py:34: invalid syntax

     ^

loco_directory/services/__init__.py:
W:103:JSONEncoder.get_field_value: Redefining built-in 'object'

Revision history for this message
Efrain Valles (effie-jayx) wrote :

I have checked dholbach's remarks and I back his request for information

Why is most code in __init__.py?

I can help with fixing the two issues if mhall ack's them

review: Needs Fixing
154. By Michael Hall <mhall@mhall-laptop>

Fixed indentation and renamed variable 'object' to 'o'

Revision history for this message
Michael Hall (mhall119) wrote :

The code is in __init__.py because I wanted the functions in the services.* namespace.

Revision history for this message
Daniel Holbach (dholbach) wrote :

Approved.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'loco_directory/services'
2=== added file 'loco_directory/services/__init__.py'
3--- loco_directory/services/__init__.py 1970-01-01 00:00:00 +0000
4+++ loco_directory/services/__init__.py 2010-06-17 11:55:43 +0000
5@@ -0,0 +1,134 @@
6+# Portions of this code comes from the Oct09 project
7+# developed by the Moffitt Cancer Center.
8+# License: http://bitbucket.org/oct09/main/src/tip/COPYING
9+# Project: http://bitbucket.org/oct09/main/wiki/Home
10+
11+from django.db.models import Model
12+from django.http import HttpResponse
13+from django.forms.fields import EMPTY_VALUES
14+from django.utils import simplejson
15+import re
16+import decimal
17+
18+def model_service(model, request, url, include=None, exclude=None):
19+ (instance_id, rem_url) = get_model_id(request, url)
20+
21+ try:
22+ if instance_id is not None and instance_id != '':
23+ return model_entity(model, request, url, include, exclude)
24+ else:
25+ return model_collection(model, request, url, include, exclude)
26+ except Exception, e:
27+ return HttpResponse(encode_error(e))
28+
29+def model_entity(model, request, url, include=None, exclude=None):
30+ (entity_id, rem_url) = get_model_id(request, url)
31+
32+ try:
33+ entity = model.objects.get(pk=entity_id)
34+ except:
35+ return HttpResponse(encode_error("Entity not found"))
36+
37+
38+ # Otherwise process the entity request
39+ if request.method == "GET":
40+ return HttpResponse(encode(entity, include, exclude), mimetype=get_mimetype())
41+
42+ elif request.method == "PUT":
43+ return HttpResponse(encode_error("Write operations are not supported"))
44+
45+ elif request.method == "DELETE":
46+ return HttpResponse(encode_error("Write operations are not supported"))
47+
48+ return HttpResponse(encode_error("%s Not Implemented"%request.method))
49+
50+def model_collection(model, request, url, include=None, exclude=None):
51+ if request.method == "GET":
52+ results = do_search(model, request.GET, include, exclude)
53+
54+ collection = list(results)
55+ return HttpResponse(encode(collection, include, exclude), mimetype=get_mimetype())
56+
57+ elif request.method == "POST":
58+ return HttpResponse(encode_error("Write operations are not supported"))
59+
60+ return HttpResponse(encode_error("%s Not Implemented"%request.method))
61+
62+def do_search(model, request, include=None, exclude=None):
63+ search_fields = include
64+ if search_fields is None:
65+ search_fields = [f.name for f in model._meta.local_fields+model._meta.many_to_many if not f.name.endswith("_ptr")]
66+ if exclude is not None:
67+ search_fields = [f for f in search_fields if f not in exclude]
68+ search_values = dict([(str(key), value) for (key, value) in request.items()])
69+
70+ results = model.objects.filter(**search_values)
71+
72+ if '_sortby' in request and request['_sortby'] not in EMPTY_VALUES:
73+ results = results.order_by(request.get('_sortby'))
74+ return results
75+
76+def get_model_id(request, url):
77+ instance_id = None
78+ rem_url = None
79+ if url is not None and len(url) > 0:
80+ m = re.match(r"^([^/]+)/?(.*)", url)
81+
82+ if m is not None:
83+ instance_id = m.group(1)
84+ rem_url = m.group(2)
85+ return (instance_id, rem_url)
86+
87+def encode(entity, include=None, exclude=None):
88+ json = JSONEncoder(include, exclude)
89+ return json.encode(entity)
90+
91+def decode(klass, entity, request, url=None):
92+ return request.raw_post_data
93+
94+def get_mimetype():
95+ return "application/json"
96+
97+def encode_error(error):
98+ json = JSONEncoder()
99+ return json.encode({'error': unicode(error)})
100+
101+class JSONEncoder(simplejson.JSONEncoder):
102+
103+ def __init__(self, include=None, exclude=None):
104+ self.include = include
105+ self.exclude = exclude
106+ super(JSONEncoder, self).__init__()
107+
108+ def get_field_value(self, o, field):
109+ f = getattr(o, field)
110+ try:
111+ if isinstance(f, Model):
112+ if hasattr(f, 'pk'):
113+ return self.default(f.pk)
114+ else:
115+ return None
116+ if isinstance(f, decimal.Decimal):
117+ return o._meta.get_field_by_name(field)[0]._format(f)
118+ else:
119+ return self.default(f)
120+ except Exception:
121+ return None
122+
123+ def default(self, o=None):
124+
125+ if isinstance(o, Model):
126+ model_fields = self.include
127+ if model_fields is None:
128+ model_fields = [f.name for f in o._meta.fields+o._meta.many_to_many if not f.name.endswith("_ptr")]
129+ if self.exclude is not None:
130+ model_fields = [f for f in model_fields if f not in self.exclude]
131+ d = dict([(field, self.get_field_value(o, field)) for field in model_fields])
132+ return d
133+ elif o.__class__.__name__ == 'ManyRelatedManager' or o.__class__.__name__ == 'RelatedManager':
134+ return [r.pk for r in o.all()]
135+ elif isinstance(o, (int, long, float)):
136+ return o
137+ elif o is None:
138+ return None
139+ return str(o)
140
141=== added file 'loco_directory/services/urls.py'
142--- loco_directory/services/urls.py 1970-01-01 00:00:00 +0000
143+++ loco_directory/services/urls.py 2010-06-17 11:55:43 +0000
144@@ -0,0 +1,20 @@
145+'''
146+Created on Jun 2, 2010
147+
148+@author: mhall
149+'''
150+from django.conf.urls.defaults import *
151+
152+urlpatterns = patterns('',
153+ #venues
154+ url(r'^teams/(.*)$', 'services.views.team_service', name='team_service'),
155+ url(r'^events/(.*)$', 'services.views.team_event_service', name='team_event_service'),
156+ url(r'^global/(.*)$', 'services.views.global_event_service', name='global_event_service'),
157+ url(r'^comments/(.*)$', 'services.views.event_comment_service', name='event_comment_service'),
158+ url(r'^attendees/(.*)$', 'services.views.event_attendee_service', name='event_attendee_service'),
159+ url(r'^venues/(.*)$', 'services.views.venue_service', name='venue_service'),
160+ url(r'^users/(.*)$', 'services.views.user_service', name='user_service'),
161+ url(r'^groups/(.*)$', 'services.views.group_service', name='group_service'),
162+ url(r'^admins/(.*)$', 'services.views.admin_service', name='admin_service'),
163+)
164+
165
166=== added file 'loco_directory/services/views.py'
167--- loco_directory/services/views.py 1970-01-01 00:00:00 +0000
168+++ loco_directory/services/views.py 2010-06-17 11:55:43 +0000
169@@ -0,0 +1,34 @@
170+from teams.models import Team
171+from events.models import GlobalEvent, TeamEvent, TeamEventComment, Attendee, TeamAdministrator
172+from venues.models import Venue
173+from django.contrib.auth.models import User, Group
174+
175+from services import model_service
176+
177+def team_service(request, url):
178+ return model_service(Team, request, url)
179+
180+def team_event_service(request, url):
181+ return model_service(TeamEvent, request, url)
182+
183+def global_event_service(request, url):
184+ return model_service(GlobalEvent, request, url)
185+
186+def event_attendee_service(request, url):
187+ return model_service(Attendee, request, url)
188+
189+def event_comment_service(request, url):
190+ return model_service(TeamEventComment, request, url)
191+
192+def venue_service(request, url):
193+ return model_service(Venue, request, url)
194+
195+def user_service(request, url):
196+ return model_service(User, request, url, include=['id', 'username', 'groups'])
197+
198+def group_service(request, url):
199+ return model_service(Group, request, url, exclude=['permissions'])
200+
201+def admin_service(request, url):
202+ return model_service(TeamAdministrator, request, url)
203+
204
205=== modified file 'loco_directory/urls.py'
206--- loco_directory/urls.py 2009-12-22 23:09:58 +0000
207+++ loco_directory/urls.py 2010-06-17 11:55:43 +0000
208@@ -15,6 +15,7 @@
209 url(r'^venues/', include('venues.urls')),
210 url(r'^logout$', 'common.views.site_logout'),
211 url(r'^jsi18n', 'django.views.i18n.javascript_catalog', name='jsi18n'),
212+ url(r'^services/', include('services.urls')),
213 )
214
215 if settings.STATIC_SERVE:

Subscribers

People subscribed via source and target branches