Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #889 from glogiotatidis/965828

API and Django upgrade
commit 4daf2ba049badc22c93317ee73a6408e59852e92 2 parents 5bf7660 + 2ba77e9
Giorgos Logiotatidis authored April 18, 2014
8  bin/jenkins.sh
@@ -81,6 +81,14 @@ ES_INDEXES = dict(default='test_${JOB_NAME}', public='test_${JOB_NAME}_public')
81 81
 ES_TIMEOUT = 60
82 82
 
83 83
 COMPRESS_ENABLED = False
  84
+SECRET_KEY = 'localdev'
  85
+
  86
+CACHES = {
  87
+    'default': {
  88
+        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
  89
+        'LOCATION': 'unique-snowflake'
  90
+    }
  91
+}
84 92
 
85 93
 SETTINGS
86 94
 
110  docs/api/api-cities.rst
Source Rendered
... ...
@@ -1,110 +0,0 @@
1  
-.. This Source Code Form is subject to the terms of the Mozilla Public
2  
-.. License, v. 2.0. If a copy of the MPL was not distributed with this
3  
-.. file, You can obtain one at http://mozilla.org/MPL/2.0/.
4  
-
5  
-.. _api-cities:
6  
-
7  
-==================
8  
-Cities
9  
-==================
10  
-
11  
-The ``cities`` method of the :doc:`Mozillians API </api>` returns information about cities.
12  
-
13  
-**Requires Authentication**
14  
-    Yes
15  
-
16  
-**Authorized Applications**
17  
-    Mozilla Corporation sites
18  
-
19  
-Endpoint
20  
---------
21  
-
22  
-    ``https://mozillians.org/api/v1/cities/``
23  
-
24  
-Parameters
25  
-----------
26  
-
27  
-    ``app_key``
28  
-        *Required* **string** - The application's API key
29  
-
30  
-    ``app_name``
31  
-        *Required* **string** - The application's name
32  
-
33  
-    ``city``
34  
-        *Optional* **string** - The city name to filter by
35  
-
36  
-    ``country``
37  
-        *Optional* **string** - The country abbreviation to filter by
38  
-
39  
-    ``order_by``
40  
-        *Optional* **string** - The attribute to sort responses by, and the order to display them in
41  
-
42  
-    ``limit``
43  
-        *Optional* **integer** - Return some number of results **Max: 500, Default: 20**
44  
-
45  
-    ``offset``
46  
-        *Optional* **integer** - Skip some number of results **Default: 0**
47  
-
48  
-    ``format``
49  
-        *Optional* **string (json/jsonp)** - Format of the response **Default: json**
50  
-
51  
-Return Codes
52  
-------------
53  
-
54  
-    ====  ===========
55  
-    Code  Description
56  
-    ====  ===========
57  
-    200:  OK Success!
58  
-    401:  Wrong app_name or app_key OR application not activated OR application not authorized 
59  
-    ====  ===========
60  
-
61  
-Examples
62  
---------
63  
-
64  
-**Get cities, sorted by the number of Mozillians in the city, ordered from most to fewest Mozillians, limited to 3 results:**
65  
-
66  
-    Request::
67  
-
68  
-        /api/v1/cities/?app_name=foobar&app_key=12345&order_by=-population&limit=3
69  
-
70  
-    Response::
71  
-
72  
-        {
73  
-            "meta": {
74  
-                "limit": 3,
75  
-                "next": "/api/v1/cities/?order_by=-population&app_name=foobar&app_key=12345&limit=3&offset=3",
76  
-                "offset": 0,
77  
-                "previous": null,
78  
-                "total_count": 749
79  
-            },
80  
-            "objects": 
81  
-            [
82  
-                {
83  
-                    "city": "San Francisco",
84  
-                    "population": 259,
85  
-                    "country": "us",
86  
-                    "country_name": "United States"
87  
-                },
88  
-                {
89  
-                    "city": "Bangalore",
90  
-                    "population": 113,
91  
-                    "country": "in",
92  
-                    "country_name": "India"
93  
-                },
94  
-                {
95  
-                    "city": "Toronto",
96  
-                    "population": 104,
97  
-                    "country": "ca",
98  
-                    "country_name": "Canada"
99  
-                }
100  
-            ]
101  
-        }
102  
-
103  
-**Get cities, ordered from most to fewest Mozillians, then by city name, then by country**::
104  
-
105  
-    /api/v1/cities/?app_name=foobar&app_key=12345&order_by=-population,city,country
106  
-
107  
-**Get cities in Greece**::
108  
-
109  
-    /api/v1/cities/?app_name=foobar&app_key=12345&country=gr
110  
-
96  docs/api/api-countries.rst
Source Rendered
... ...
@@ -1,96 +0,0 @@
1  
-.. This Source Code Form is subject to the terms of the Mozilla Public
2  
-.. License, v. 2.0. If a copy of the MPL was not distributed with this
3  
-.. file, You can obtain one at http://mozilla.org/MPL/2.0/.
4  
-
5  
-.. _api-countries:
6  
-
7  
-==================
8  
-Countries
9  
-==================
10  
-
11  
-The ``countries`` method of the :doc:`Mozillians API </api>` returns information about countries.
12  
-
13  
-**Requires Authentication**
14  
-    Yes
15  
-
16  
-**Authorized Applications**
17  
-    Mozilla Corporation sites
18  
-
19  
-Endpoint
20  
---------
21  
-
22  
-    ``https://mozillians.org/api/v1/countries/``
23  
-
24  
-Parameters
25  
-----------
26  
-
27  
-    ``app_key``
28  
-        *Required* **string** - The application's API key
29  
-
30  
-    ``app_name``
31  
-        *Required* **string** - The application's name
32  
-
33  
-    ``order_by``
34  
-        *Optional* **string** - The attribute to sort responses by, and the order to display them in
35  
-
36  
-    ``limit``
37  
-        *Optional* **integer** - Return some number of results **Max: 500, Default: 20**
38  
-
39  
-    ``offset``
40  
-        *Optional* **integer** - Skip some number of results **Default: 0**
41  
-
42  
-    ``format``
43  
-        *Optional* **string (json/jsonp)** - Format of the response **Default: json**
44  
-
45  
-Return Codes
46  
-------------
47  
-
48  
-    ====  ===========
49  
-    Code  Description
50  
-    ====  ===========
51  
-    200:  OK Success!
52  
-    401:  Wrong app_name or app_key OR application not activated OR application not authorized 
53  
-    ====  ===========
54  
-
55  
-Examples
56  
---------
57  
-
58  
-**Get countries, sorted by the number of Mozillians in the country, ordered from most to fewest Mozillians, limited to 3 results:**
59  
-
60  
-    Request::
61  
-
62  
-        /api/v1/countries/?app_name=foobar&app_key=12345&order_by=-population&limit=3
63  
-
64  
-    Response::
65  
-
66  
-        {
67  
-            "meta": {
68  
-                "limit": 3,
69  
-                "next": "/api/v1/countries/?order_by=-population&app_name=foobar&app_key=12345&limit=3&offset=3",
70  
-                "offset": 0,
71  
-                "previous": null,
72  
-                "total_count": 749
73  
-            },
74  
-            "objects": 
75  
-            [
76  
-                {
77  
-                    "population": 259,
78  
-                    "country": "us",
79  
-                    "country_name": "United States"
80  
-                },
81  
-                {
82  
-                    "population": 113,
83  
-                    "country": "in",
84  
-                    "country_name": "India"
85  
-                },
86  
-                {
87  
-                    "population": 104,
88  
-                    "country": "ca",
89  
-                    "country_name": "Canada"
90  
-                }
91  
-            ]
92  
-        }
93  
-
94  
-**Get countries, ordered from most to fewest Mozillians, then by country abbreviation**::
95  
-
96  
-    /api/v1/countries/?app_name=foobar&app_key=12345&order_by=-population,country
106  docs/api/api-languages.rst
Source Rendered
... ...
@@ -1,106 +0,0 @@
1  
-.. This Source Code Form is subject to the terms of the Mozilla Public
2  
-.. License, v. 2.0. If a copy of the MPL was not distributed with this
3  
-.. file, You can obtain one at http://mozilla.org/MPL/2.0/.
4  
-
5  
-.. _api-languages:
6  
-
7  
-==================
8  
-Languages
9  
-==================
10  
-
11  
-The ``languages`` method of the :doc:`Mozillians API </api>` returns information about languages.
12  
-
13  
-**Requires Authentication**
14  
-    Yes
15  
-
16  
-**Authorized Applications**
17  
-    Mozilla Corporation sites
18  
-
19  
-Endpoint
20  
---------
21  
-
22  
-    ``https://mozillians.org/api/v1/languages/``
23  
-
24  
-Parameters
25  
-----------
26  
-
27  
-    ``app_key``
28  
-        *Required* **string** - The application's API key
29  
-
30  
-    ``app_name``
31  
-        *Required* **string** - The application's name
32  
-
33  
-    ``order_by``
34  
-        *Optional* **string** - The attribute to sort responses by, and the order to display them in
35  
-
36  
-    ``limit``
37  
-        *Optional* **integer** - Return some number of results **Max: 500, Default: 20**
38  
-
39  
-    ``offset``
40  
-        *Optional* **integer** - Skip some number of results **Default: 0**
41  
-
42  
-    ``format``
43  
-        *Optional* **string (json/jsonp)** - Format of the response **Default: json**
44  
-
45  
-Return Codes
46  
-------------
47  
-
48  
-    ====  ===========
49  
-    Code  Description
50  
-    ====  ===========
51  
-    200:  OK Success!
52  
-    401:  Wrong app_name or app_key OR application not activated OR application not authorized 
53  
-    ====  ===========
54  
-
55  
-Examples
56  
---------
57  
-
58  
-**Get languages, sorted by the number of members who speak the language, ordered from most to fewest members, limited to 3 results:**
59  
-
60  
-    Request::
61  
-
62  
-        /api/v1/languages/?app_name=foobar&app_key=12345&order_by=-number_of_members&limit=3
63  
-
64  
-    Response::
65  
-
66  
-        {
67  
-            "meta": {
68  
-                "limit": 3,
69  
-                "next": "/api/v1/languages/?order_by=-number_of_members&app_name=foobar&app_key=12345&limit=3&offset=3",
70  
-                "offset": 0,
71  
-                "previous": null,
72  
-                "total_count": 749
73  
-            },
74  
-            "objects": 
75  
-            [
76  
-                {
77  
-                    "id": "471",
78  
-                    "name": "english",
79  
-                    "number_of_members": 2159,
80  
-                    "resource_uri": "/api/v1/languages/471/",
81  
-                    "url": "https://mozillians.org/language/english/"
82  
-                },
83  
-                {
84  
-                    "id": "18",
85  
-                    "name": "french",
86  
-                    "number_of_members": 1513,
87  
-                    "resource_uri": "/api/v1/languages/18/",
88  
-                    "url": "https://mozillians.org/language/french/"
89  
-                },
90  
-                {
91  
-                    "id": "1907",
92  
-                    "name": "vulcan",
93  
-                    "number_of_members": 183,
94  
-                    "resource_uri": "/api/v1/languages/1907/",
95  
-                    "url": "https://mozillians.org/language/vulcan/"
96  
-                }
97  
-            ]
98  
-        }
99  
-
100  
-**Get languages, ordered from most to fewest members and then by language name**::
101  
-
102  
-    /api/v1/languages/?app_name=foobar&app_key=12345&order_by=-number_of_members,name
103  
-
104  
-**Get language having id 509**::
105  
-
106  
-    /api/v1/languages/509/?app_name=foobar&app_key=12345
3  docs/api/api.rst
Source Rendered
@@ -43,6 +43,3 @@ API Methods
43 43
    api-users
44 44
    api-groups
45 45
    api-skills
46  
-   api-languages
47  
-   api-cities
48  
-   api-countries
21  mozillians/api/authenticators.py
@@ -9,15 +9,22 @@ class AppAuthentication(Authentication):
9 9
     """App Authentication."""
10 10
 
11 11
     def is_authenticated(self, request, **kwargs):
12  
-        """Authenticate App."""
  12
+        """Authenticate and authorize App."""
13 13
         app_key = request.GET.get('app_key', '')
14 14
         app_name = request.GET.get('app_name', '')
15 15
 
16  
-        result = (APIApp.objects.filter(name__iexact=app_name, key=app_key,
17  
-                                        is_active=True).exists())
18  
-        if result:
19  
-            statsd.incr('api.auth.success')
20  
-        else:
  16
+        try:
  17
+            app = APIApp.objects.get(name__iexact=app_name, key=app_key, is_active=True)
  18
+        except APIApp.DoesNotExist:
21 19
             statsd.incr('api.auth.failed')
  20
+            return False
22 21
 
23  
-        return result
  22
+        statsd.incr('api.auth.success')
  23
+        if not app.is_mozilla_app:
  24
+            statsd.incr('api.requests.total_community')
  25
+            data = request.GET.copy()
  26
+            data['restricted'] = True
  27
+            request.GET = data
  28
+        else:
  29
+            statsd.incr('api.requests.total_mozilla')
  30
+        return True
32  mozillians/api/authorisers.py
... ...
@@ -1,32 +0,0 @@
1  
-from django_statsd.clients import statsd
2  
-from tastypie.authorization import ReadOnlyAuthorization
3  
-
4  
-from mozillians.api.models import APIApp
5  
-
6  
-
7  
-class MozillaOfficialAuthorization(ReadOnlyAuthorization):
8  
-    """Authorize an App as official Mozilla or Community."""
9  
-
10  
-    def is_authorized(self, request, object=None):
11  
-        """Authorize App.
12  
-
13  
-        Always authorize Apps. Community Apps get a 'restricted' URL
14  
-        parameter.
15  
-
16  
-        """
17  
-        app_name = request.GET.get('app_name', None)
18  
-        app_key = request.GET.get('app_key', None)
19  
-        app = APIApp.objects.get(name=app_name, key=app_key)
20  
-
21  
-        statsd.incr('api.requests.total')
22  
-        statsd.incr('api.requests.app.%d' % app.id)
23  
-
24  
-        if not app.is_mozilla_app:
25  
-            statsd.incr('api.requests.total_community')
26  
-            data = request.GET.copy()
27  
-            data['restricted'] = True
28  
-            request.GET = data
29  
-        else:
30  
-            statsd.incr('api.requests.total_mozilla')
31  
-
32  
-        return True
16  mozillians/api/tests/test_authenticators.py
@@ -48,3 +48,19 @@ def test_invalid_app_name_and_key(self):
48 48
         request.GET = {'app_key': 'invalid', 'app_name': 'invalid'}
49 49
         authentication = AppAuthentication()
50 50
         eq_(authentication.is_authenticated(request), False)
  51
+
  52
+    def test_mozilla_app(self):
  53
+        app = APIAppFactory.create(is_mozilla_app=True)
  54
+        request = RequestFactory()
  55
+        request.GET = {'app_key': app.key, 'app_name': app.name}
  56
+        authentication = AppAuthentication()
  57
+        authentication.is_authenticated(request)
  58
+        eq_(request.GET.get('restricted'), None)
  59
+
  60
+    def test_community_app(self):
  61
+        app = APIAppFactory.create(is_mozilla_app=False)
  62
+        request = RequestFactory()
  63
+        request.GET = {'app_key': app.key, 'app_name': app.name}
  64
+        authentication = AppAuthentication()
  65
+        authentication.is_authenticated(request)
  66
+        eq_(request.GET.get('restricted'), True)
2  mozillians/api/urls.py
@@ -8,8 +8,6 @@
8 8
 
9 9
 v1_api = Api(api_name='v1')
10 10
 v1_api.register(mozillians.users.api.UserResource())
11  
-v1_api.register(mozillians.users.api.CountryResource())
12  
-v1_api.register(mozillians.users.api.CityResource())
13 11
 v1_api.register(mozillians.groups.api.GroupResource())
14 12
 v1_api.register(mozillians.groups.api.SkillResource())
15 13
 
4  mozillians/groups/api.py
@@ -3,11 +3,11 @@
3 3
 
4 4
 from funfactory import utils
5 5
 from tastypie import fields
  6
+from tastypie.authorization import ReadOnlyAuthorization
6 7
 from tastypie.resources import ModelResource
7 8
 from tastypie.serializers import Serializer
8 9
 
9 10
 from mozillians.api.authenticators import AppAuthentication
10  
-from mozillians.api.authorisers import MozillaOfficialAuthorization
11 11
 from mozillians.api.resources import (AdvancedSortingResourceMixIn,
12 12
                                       ClientCacheResourceMixIn,
13 13
                                       GraphiteMixIn)
@@ -22,7 +22,7 @@ class GroupBaseResource(AdvancedSortingResourceMixIn, ClientCacheResourceMixIn,
22 22
 
23 23
     class Meta:
24 24
         authentication = AppAuthentication()
25  
-        authorization = MozillaOfficialAuthorization()
  25
+        authorization = ReadOnlyAuthorization()
26 26
         cache_control = {'max-age': 0}
27 27
         list_allowed_methods = ['get']
28 28
         detail_allowed_methods = ['get']
7  mozillians/phonebook/urls.py
... ...
@@ -1,5 +1,5 @@
1 1
 from django.conf.urls.defaults import patterns, url
2  
-from django.views.generic.simple import direct_to_template
  2
+from django.views.generic import TemplateView
3 3
 
4 4
 from mozillians.common.decorators import allow_public
5 5
 
@@ -33,6 +33,7 @@
33 33
 
34 34
 
35 35
     # Static pages need csrf for browserID post to work
36  
-    url(r'^about/$', allow_public(direct_to_template),
37  
-        {'template': 'phonebook/about.html'}, name='about'),
  36
+    url(r'^about/$',
  37
+        allow_public(TemplateView.as_view(template_name='phonebook/about.html')),
  38
+        name='about'),
38 39
 )
2  mozillians/settings/base.py
@@ -181,8 +181,6 @@ def JINJA_CONFIG():
181 181
     }
182 182
 }
183 183
 
184  
-AUTH_PROFILE_MODULE = 'users.UserProfile'
185  
-
186 184
 MAX_PHOTO_UPLOAD_SIZE = 8 * (1024 ** 2)
187 185
 
188 186
 AUTO_VOUCH_DOMAINS = ('mozilla.com', 'mozilla.org', 'mozillafoundation.org')
9  mozillians/settings/local.py-devdist
@@ -71,3 +71,12 @@ BASKET_MANAGERS = None  # or list of email addresses.
71 71
 GA_ACCOUNT_CODE = None
72 72
 
73 73
 COMPRESS_ENABLED = False
  74
+
  75
+SECRET_KEY = 'localdev'
  76
+
  77
+CACHES = {
  78
+    'default': {
  79
+        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
  80
+        'LOCATION': 'unique-snowflake'
  81
+    }
  82
+}
2  mozillians/templates/admin/base_site.html
@@ -13,5 +13,5 @@ <h1 id="site-name">Mozillians Administration</h1>
13 13
 {% endblock %}
14 14
 
15 15
 {% block userlinks %}
16  
-  <a href="{% url phonebook:logout %}">{% trans 'Log out' %}</a>
  16
+  <a href="{% url 'phonebook:logout' %}">{% trans 'Log out' %}</a>
17 17
 {% endblock %}
2  mozillians/templates/admin/users/userprofile/change_list.html
@@ -3,7 +3,7 @@
3 3
 {% block object-tools-items %}
4 4
   {{ block.super }}
5 5
   <li>
6  
-    <a href="{% url admin:users_index_profiles %}">
  6
+    <a href="{% url 'admin:users_index_profiles' %}">
7 7
       Update Elastic Search Index
8 8
     </a>
9 9
   </li>
2  mozillians/templates/base.html
@@ -104,7 +104,7 @@
104 104
                             {{ _('Edit Your Profile') }}
105 105
                           </a>
106 106
                         </li>
107  
-                        {% if user.get_profile().is_vouched %}
  107
+                        {% if user.userprofile.is_vouched %}
108 108
                           <li>
109 109
                             <a id="invite" href="{{ url('phonebook:invite') }}">
110 110
                               <i class="icon-group"></i>
2  mozillians/templates/phonebook/edit_profile.html
@@ -344,7 +344,7 @@
344 344
         {{ profile_form.allows_mozilla_sites }}
345 345
       </p>
346 346
 
347  
-      {% if user.get_profile().is_vouched %}
  347
+      {% if user.userprofile.is_vouched %}
348 348
         <h5>{{ _('Developers') }}</h5>
349 349
         <p class="field_description">
350 350
           {% trans api_schema_url='http://mozillians.readthedocs.org/en/latest/api.html',
4  mozillians/templates/phonebook/profile.html
@@ -159,7 +159,7 @@ <h1 class="p-name">
159 159
           <div id="groups">
160 160
             <h3><i class="icon-group"></i> {{ _('Groups') }}</h3>
161 161
               {% for group in groups %}
162  
-                {% if (user.is_authenticated() and user.get_profile().is_vouched) %}
  162
+                {% if (user.is_authenticated() and user.userprofile.is_vouched) %}
163 163
                   <a href="{{ url('groups:show_group', group.url) }}">
164 164
                     {%- if group.curator == profile -%}
165 165
                       <i class="icon-certificate"></i>
@@ -181,7 +181,7 @@ <h1 class="p-name">
181 181
             <h3><i class="icon-wrench"></i> {{ _('Skills') }}</h3>
182 182
               {% for skill in profile.skills.all() %}
183 183
                 {% if (user.is_authenticated() and
184  
-                       user.get_profile().is_vouched) %}
  184
+                       user.userprofile.is_vouched) %}
185 185
                   <a href="{{ url('groups:show_skill', skill.url) }}">{{ skill.name }}</a>
186 186
                 {%- else -%}
187 187
                   {{ skill.name }}
231  mozillians/users/api.py
... ...
@@ -1,240 +1,23 @@
1  
-from collections import namedtuple
2  
-from operator import itemgetter
3 1
 from urllib2 import unquote
4 2
 from urlparse import urljoin
5 3
 
6 4
 from django.conf import settings
7 5
 from django.core.urlresolvers import reverse
8  
-from django.db.models import Count, Q
  6
+from django.db.models import Q
9 7
 
10 8
 from funfactory import utils
11  
-from tastypie import fields
12  
-from tastypie import http
  9
+from tastypie import fields, http
  10
+from tastypie.authorization import ReadOnlyAuthorization
13 11
 from tastypie.bundle import Bundle
14 12
 from tastypie.exceptions import ImmediateHttpResponse
15  
-from tastypie.resources import ModelResource, Resource
  13
+from tastypie.resources import ModelResource
16 14
 from tastypie.serializers import Serializer
17 15
 
18 16
 from mozillians.api.authenticators import AppAuthentication
19  
-from mozillians.api.authorisers import MozillaOfficialAuthorization
20 17
 from mozillians.api.paginator import Paginator
21  
-from mozillians.api.resources import (AdvancedSortingResourceMixIn,
22  
-                                      ClientCacheResourceMixIn,
  18
+from mozillians.api.resources import (ClientCacheResourceMixIn,
23 19
                                       GraphiteMixIn)
24  
-from mozillians.users.models import COUNTRIES, UserProfile
25  
-
26  
-
27  
-Country = namedtuple('Country', ['country', 'country_name', 'population'])
28  
-City = namedtuple('City', ['city', 'country', 'country_name', 'population'])
29  
-
30  
-
31  
-class CustomQuerySet(object):
32  
-    """A custom queryset class.
33  
-
34  
-    Supports database count() on len(), order_by, filter, array
35  
-    slicing.
36  
-
37  
-    """
38  
-
39  
-    def __init__(self, queryset):
40  
-        self._query = queryset
41  
-
42  
-    def __len__(self):
43  
-        return self._query.count()
44  
-
45  
-    def order_by(self, *args):
46  
-        return self._query.order_by(*args)
47  
-
48  
-    def filter(self, *args, **kwargs):
49  
-        return self._query.filter(*args, **kwargs)
50  
-
51  
-    def __getitem__(self, key):
52  
-        if isinstance(key, slice):
53  
-            return self._query[key.start:key.stop]
54  
-        return self._query[key]
55  
-
56  
-
57  
-class FakeQuerySet(object):
58  
-    """
59  
-    Wrap an iterable and make it work like a pretty dumb queryset.
60  
-    """
61  
-
62  
-    def __init__(self, iterable):
63  
-        # We won't be able to get the data out of this without evaluating
64  
-        # the iterable (to sort or index it), so we might as well evaluate
65  
-        # it once now.
66  
-        self.values = list(iterable)
67  
-
68  
-    def order_by(self, *args):
69  
-        return sorted(self.values, key=itemgetter(*args))
70  
-
71  
-    def __getitem__(self, key):
72  
-        if isinstance(key, slice):
73  
-            return self.values[key.start:key.stop]
74  
-        return self.values[key]
75  
-
76  
-
77  
-class LocationCustomResource(AdvancedSortingResourceMixIn,
78  
-                             ClientCacheResourceMixIn, GraphiteMixIn,
79  
-                             Resource):
80  
-
81  
-    class Meta:
82  
-        authentication = AppAuthentication()
83  
-        authorization = MozillaOfficialAuthorization()
84  
-        list_allowed_methods = ['get']
85  
-        serializer = Serializer(formats=['json', 'jsonp'])
86  
-        paginator_class = Paginator
87  
-        include_resource_uri = False
88  
-        detail_allowed_methods = []
89  
-        cache_control = {'max-age': 0}
90  
-
91  
-    def get_object_list(self):
92  
-        queryset = self.Meta.queryset
93  
-        return CustomQuerySet(queryset)
94  
-
95  
-    def obj_get_list(self, request=None, **kwargs):
96  
-        obj_list = self.get_object_list()
97  
-        filters = self.build_filters(getattr(request, 'GET', None))
98  
-        if filters:
99  
-            obj_list = self.apply_filters(obj_list, filters)
100  
-        return obj_list
101  
-
102  
-    def apply_filters(self, obj_list, applicable_filters):
103  
-        mega_filter = Q()
104  
-        for db_filter in applicable_filters.values():
105  
-            mega_filter &= db_filter
106  
-        return obj_list.filter(mega_filter)
107  
-
108  
-
109  
-def collapse_locations(obj_list, keyname):
110  
-    """
111  
-    Given a CustomQuerySet object, filter/aggregate it down
112  
-    so we just have one item per country or city.
113  
-
114  
-    keyname is 'country' or 'city'.
115  
-
116  
-    Also drop the 'privacy_country' or 'privacy_city' field.
117  
-
118  
-    On input, we might have:
119  
-
120  
-       country   privacy_country  population
121  
-         Gr             1            27
122  
-         Gr             2            16
123  
-         Us             1            13
124  
-         Us             2            12
125  
-
126  
-    and we want to end up with
127  
-
128  
-       country   population
129  
-         Gr          43
130  
-         Us          25
131  
-
132  
-    Returns a CustomQuerySet.
133  
-    """
134  
-
135  
-    locations = {}
136  
-    delkey = 'privacy_%s' % keyname
137  
-    for item in obj_list:
138  
-        location = item[keyname]
139  
-        if location in locations:
140  
-            locations[location]['population'] += item['population']
141  
-        else:
142  
-            if delkey in item:
143  
-                del item[delkey]
144  
-            locations[location] = item
145  
-    # Turn the dictionary into an iterable of the dict values.
146  
-    locations = locations.itervalues()
147  
-    # Now we have an iterable of dicts, but an iterable is not a queryset
148  
-    queryset = FakeQuerySet(locations)
149  
-    # And let's make it a CustomQuerySet again
150  
-    queryset = CustomQuerySet(queryset)
151  
-    return queryset
152  
-
153  
-
154  
-class CountryResource(LocationCustomResource):
155  
-    country = fields.CharField(attribute='country')
156  
-    country_name = fields.CharField(attribute='country_name')
157  
-    population = fields.IntegerField(attribute='population')
158  
-    url = fields.CharField()
159  
-
160  
-    class Meta(LocationCustomResource.Meta):
161  
-        resource_name = 'countries'
162  
-        ordering = ['country', 'population']
163  
-        queryset = (UserProfile.objects
164  
-                    .vouched()
165  
-                    .exclude(country='')
166  
-                    .values('country', 'privacy_country')
167  
-                    .annotate(population=Count('country')))
168  
-        default_order = ('country',)
169  
-        object_class = Country
170  
-
171  
-    def obj_get_list(self, request=None, **kwargs):
172  
-        obj_list = super(CountryResource, self).obj_get_list(request, **kwargs)
173  
-        return collapse_locations(obj_list, 'country')
174  
-
175  
-    def build_filters(self, filters=None):
176  
-        return None
177  
-
178  
-    def full_dehydrate(self, queryset):
179  
-        queryset.obj = self.Meta.object_class(
180  
-            country=queryset.obj['country'],
181  
-            population=queryset.obj['population'],
182  
-            country_name=COUNTRIES[queryset.obj['country']])
183  
-        return super(LocationCustomResource, self).full_dehydrate(queryset)
184  
-
185  
-    def dehydrate_url(self, bundle):
186  
-        url = reverse('phonebook:list_country', args=[bundle.obj.country])
187  
-        return utils.absolutify(url)
188  
-
189  
-
190  
-class CityResource(LocationCustomResource):
191  
-    city = fields.CharField(attribute='city')
192  
-    country = fields.CharField(attribute='country')
193  
-    country_name = fields.CharField(attribute='country_name')
194  
-    population = fields.IntegerField(attribute='population')
195  
-    url = fields.CharField()
196  
-
197  
-    class Meta(LocationCustomResource.Meta):
198  
-        resource_name = 'cities'
199  
-        ordering = ['city', 'country', 'population']
200  
-        queryset = (UserProfile.objects
201  
-                    .vouched()
202  
-                    .exclude(city='')
203  
-                    .exclude(country='')
204  
-                    .values('city', 'privacy_city',
205  
-                            'country', 'privacy_country')
206  
-                    .annotate(population=Count('city')))
207  
-        default_order = ('country', 'city')
208  
-        object_class = City
209  
-
210  
-    def obj_get_list(self, request=None, **kwargs):
211  
-        obj_list = super(CityResource, self).obj_get_list(request, **kwargs)
212  
-        return collapse_locations(obj_list, 'city')
213  
-
214  
-    def build_filters(self, filters=None):
215  
-        database_filters = {}
216  
-        valid_filters = [f for f in filters if f in ['country', 'city']]
217  
-        getvalue = lambda x: unquote(filters[x].lower())
218  
-
219  
-        for valid_filter in valid_filters:
220  
-            database_filters[valid_filter] = (
221  
-                Q(**{'{0}__iexact'.format(valid_filter):
222  
-                     getvalue(valid_filter)}))
223  
-
224  
-        return database_filters
225  
-
226  
-    def full_dehydrate(self, queryset):
227  
-        queryset.obj = self.Meta.object_class(
228  
-            country=queryset.obj['country'],
229  
-            country_name=COUNTRIES[queryset.obj['country']],
230  
-            population=queryset.obj['population'],
231  
-            city=queryset.obj['city'])
232  
-        return super(LocationCustomResource, self).full_dehydrate(queryset)
233  
-
234  
-    def dehydrate_url(self, bundle):
235  
-        url = reverse('phonebook:list_city',
236  
-                      args=[bundle.obj.country, bundle.obj.city])
237  
-        return utils.absolutify(url)
  20
+from mozillians.users.models import UserProfile
238 21
 
239 22
 
240 23
 class UserResource(ClientCacheResourceMixIn, GraphiteMixIn, ModelResource):
@@ -252,7 +35,7 @@ class UserResource(ClientCacheResourceMixIn, GraphiteMixIn, ModelResource):
252 35
     class Meta:
253 36
         queryset = UserProfile.objects.all()
254 37
         authentication = AppAuthentication()
255  
-        authorization = MozillaOfficialAuthorization()
  38
+        authorization = ReadOnlyAuthorization()
256 39
         serializer = Serializer(formats=['json', 'jsonp'])
257 40
         paginator_class = Paginator
258 41
         cache_control = {'max-age': 0}
221  mozillians/users/tests/test_api.py
@@ -7,212 +7,15 @@
7 7
 
8 8
 from funfactory.helpers import urlparams
9 9
 from funfactory.utils import absolutify
10  
-from mock import patch
11 10
 from nose.tools import eq_, ok_
12 11
 
13 12
 from mozillians.api.tests import APIAppFactory
14 13
 from mozillians.common.tests import TestCase
15 14
 from mozillians.groups.tests import GroupFactory, SkillFactory
16  
-from mozillians.users.api import CustomQuerySet
17  
-from mozillians.users.managers import MOZILLIANS, PUBLIC
18 15
 from mozillians.users.models import ExternalAccount
19 16
 from mozillians.users.tests import UserFactory
20 17
 
21 18
 
22  
-class CityResourceTests(TestCase):
23  
-    def setUp(self):
24  
-        self.user = UserFactory.create()
25  
-        self.resource_url = reverse(
26  
-            'api_dispatch_list',
27  
-            kwargs={'api_name': 'v1', 'resource_name': 'cities'})
28  
-        self.app = APIAppFactory.create(owner=self.user,
29  
-                                        is_mozilla_app=True)
30  
-        self.resource_url = urlparams(self.resource_url,
31  
-                                      app_name=self.app.name,
32  
-                                      app_key=self.app.key)
33  
-
34  
-    def test_get_list(self):
35  
-        UserFactory.create(userprofile={'country': 'gr', 'city': 'Athens'})
36  
-        UserFactory.create(vouched=False, userprofile={'country': 'gr',
37  
-                                                       'city': 'Athens'})
38  
-        client = Client()
39  
-        response = client.get(self.resource_url, follow=True)
40  
-        eq_(response.status_code, 200)
41  
-        data = json.loads(response.content)
42  
-        eq_(data['meta']['total_count'], 1, 'Unvouched users get listed!')
43  
-        eq_(data['objects'][0]['city'], 'Athens')
44  
-        eq_(data['objects'][0]['country'], 'gr')
45  
-        eq_(data['objects'][0]['country_name'], 'Greece')
46  
-        eq_(data['objects'][0]['population'], 1)
47  
-        eq_(data['objects'][0]['url'],
48  
-            absolutify(reverse('phonebook:list_city', args=['gr', 'Athens'])))
49  
-
50  
-    def test_not_duplicated(self):
51  
-        # Ensure if there are users from the same city with different
52  
-        # privacy settings, the city API only returns that city once.
53  
-        # Also, the population should be the total.
54  
-        # Note that setUp() already created one User, but that User has
55  
-        # no city and so should not show up in these results.
56  
-        UserFactory.create(userprofile={'privacy_city': MOZILLIANS, 'city': 'Athens'})
57  
-        UserFactory.create(userprofile={'privacy_city': PUBLIC, 'city': 'Athens'})
58  
-        client = Client()
59  
-        response = client.get(self.resource_url, follow=True)
60  
-        eq_(response.status_code, 200)
61  
-        data = json.loads(response.content)
62  
-        eq_(data['meta']['total_count'], 1)
63  
-        eq_(data['objects'][0]['population'], 2)
64  
-        eq_(data['objects'][0]['city'], 'Athens')
65  
-
66  
-    def test_get_details(self):
67  
-        client = Client()
68  
-        url = reverse('api_dispatch_detail',
69  
-                      kwargs={'api_name': 'v1',
70  
-                              'resource_name': 'cities', 'pk': 1})
71  
-        response = client.get(url, follow=True)
72  
-        eq_(response.status_code, 405)
73  
-
74  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
75  
-           wraps=CustomQuerySet.order_by)
76  
-    def test_default_ordering(self, order_by_mock):
77  
-        client = Client()
78  
-        client.get(self.resource_url, follow=True)
79  
-        order_by_mock.assert_called_with('country', 'city')
80  
-
81  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
82  
-           wraps=CustomQuerySet.order_by)
83  
-    def test_custom_ordering(self, order_by_mock):
84  
-        client = Client()
85  
-        url = urlparams(self.resource_url, order_by='-population')
86  
-        client.get(url, follow=True)
87  
-        order_by_mock.assert_called_with('-population')
88  
-
89  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
90  
-           wraps=CustomQuerySet.order_by)
91  
-    def test_custom_invalid_ordering(self, order_by_mock):
92  
-        client = Client()
93  
-        url = urlparams(self.resource_url, order_by='foo')
94  
-        client.get(url, follow=True)
95  
-        order_by_mock.assert_called_with('country', 'city')
96  
-
97  
-    @patch('mozillians.users.api.CustomQuerySet.filter',
98  
-           wraps=CustomQuerySet.filter)
99  
-    def test_filtering(self, filter_mock):
100  
-        url = urlparams(self.resource_url, city='athens')
101  
-        client = Client()
102  
-        client.get(url, follow=True)
103  
-        ok_(filter_mock.called)
104  
-        call_arg = filter_mock.call_args[0][0]
105  
-        eq_(call_arg.children, [('city__iexact', 'athens')])
106  
-
107  
-    @patch('mozillians.users.api.CustomQuerySet.filter',
108  
-           wraps=CustomQuerySet.filter)
109  
-    def test_invalid_filtering(self, filter_mock):
110  
-        url = urlparams(self.resource_url, foo='bar')
111  
-        client = Client()
112  
-        client.get(url, follow=True)
113  
-        ok_(not filter_mock.called)
114  
-
115  
-
116  
-class CountryResourceTests(TestCase):
117  
-    def setUp(self):
118  
-        self.user = UserFactory.create()
119  
-        self.resource_url = reverse(
120  
-            'api_dispatch_list',
121  
-            kwargs={'api_name': 'v1', 'resource_name': 'countries'})
122  
-        self.app = APIAppFactory.create(owner=self.user,
123  
-                                        is_mozilla_app=True)
124  
-        self.resource_url = urlparams(self.resource_url,
125  
-                                      app_name=self.app.name,
126  
-                                      app_key=self.app.key)
127  
-
128  
-    def test_get_list(self):
129  
-        UserFactory.create(vouched=False, userprofile={'country': 'gr'})
130  
-        client = Client()
131  
-        response = client.get(self.resource_url, follow=True)
132  
-        eq_(response.status_code, 200)
133  
-        data = json.loads(response.content)
134  
-        eq_(data['meta']['total_count'], 1, 'Unvouched users get listed!')
135  
-        eq_(data['objects'][0]['country'], 'gr')
136  
-        eq_(data['objects'][0]['country_name'], 'Greece')
137  
-        eq_(data['objects'][0]['population'], 1)
138  
-        eq_(data['objects'][0]['url'],
139  
-            absolutify(reverse('phonebook:list_country', args=['gr'])))
140  
-
141  
-    def test_not_duplicated(self):
142  
-        # If mozillians from the same country have different privacy_country
143  
-        # settings, make sure we don't return the country twice in the API
144  
-        # result.
145  
-        # Also, the population should be the total.
146  
-
147  
-        # NOTE: There's already one Greek created in setUp()
148  
-
149  
-        # Create a couple more Greeks with each privacy setting.  We should
150  
-        # still get back Greece only once.
151  
-        for i in xrange(2):
152  
-            UserFactory.create(userprofile={'country': 'gr',
153  
-                                            'privacy_country': MOZILLIANS})
154  
-        for i in xrange(2):
155  
-            UserFactory.create(userprofile={'country': 'gr',
156  
-                                            'privacy_country': PUBLIC})
157  
-
158  
-        # One person from another country, to make sure that country shows up too.
159  
-        UserFactory.create(userprofile={'country': 'us',
160  
-                                        'privacy_country': MOZILLIANS})
161  
-
162  
-        client = Client()
163  
-        response = client.get(self.resource_url, follow=True)
164  
-        eq_(response.status_code, 200)
165  
-        data = json.loads(response.content)
166  
-        # We should get back Gr and Us
167  
-        eq_(data['meta']['total_count'], 2)
168  
-        for obj in data['objects']:
169  
-            if obj['country'] == 'gr':
170  
-                # 5 greeks
171  
-                eq_(obj['population'], 5)
172  
-            else:
173  
-                # 1 USian
174  
-                eq_(obj['population'], 1)
175  
-
176  
-    def test_get_details(self):
177  
-        client = Client()
178  
-        url = reverse('api_dispatch_detail',
179  
-                      kwargs={'api_name': 'v1',
180  
-                              'resource_name': 'countries', 'pk': 1})
181  
-        response = client.get(url, follow=True)
182  
-        eq_(response.status_code, 405)
183  
-
184  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
185  
-           wraps=CustomQuerySet.order_by)
186  
-    def test_default_ordering(self, order_by_mock):
187  
-        client = Client()
188  
-        client.get(self.resource_url, follow=True)
189  
-        order_by_mock.assert_called_with('country')
190  
-
191  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
192  
-           wraps=CustomQuerySet.order_by)
193  
-    def test_custom_ordering(self, order_by_mock):
194  
-        client = Client()
195  
-        url = urlparams(self.resource_url, order_by='-population')
196  
-        client.get(url, follow=True)
197  
-        order_by_mock.assert_called_with('-population')
198  
-
199  
-    @patch('mozillians.users.api.CustomQuerySet.order_by',
200  
-           wraps=CustomQuerySet.order_by)
201  
-    def test_custom_invalid_ordering(self, order_by_mock):
202  
-        client = Client()
203  
-        url = urlparams(self.resource_url, order_by='foo')
204  
-        client.get(url, follow=True)
205  
-        order_by_mock.assert_called_with('country')
206  
-
207  
-    @patch('mozillians.users.api.CustomQuerySet.filter',
208  
-           wraps=CustomQuerySet.filter)
209  
-    def test_filtering(self, filter_mock):
210  
-        url = urlparams(self.resource_url, country='gr')
211  
-        client = Client()
212  
-        client.get(url, follow=True)
213  
-        ok_(not filter_mock.called)
214  
-
215  
-
216 19
 class UserResourceTests(TestCase):
217 20
     def setUp(self):
218 21
         voucher = UserFactory.create()
@@ -262,7 +65,7 @@ def test_get_detail_mozilla_app(self):
262 65
         data = json.loads(response.content)
263 66
         profile = self.user.userprofile
264 67
         eq_(response.status_code, 200)
265  
-        eq_(data['id'], unicode(profile.id))
  68
+        eq_(data['id'], profile.id)
266 69
         eq_(data['full_name'], profile.full_name)
267 70
         eq_(data['is_vouched'], profile.is_vouched)
268 71
         eq_(data['vouched_by'], profile.vouched_by.user.id)
@@ -351,7 +154,7 @@ def test_is_vouched_false(self):
351 154
         response = client.get(url, follow=True)
352 155
         data = json.loads(response.content)
353 156
         eq_(len(data['objects']), 1)
354  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  157
+        eq_(data['objects'][0]['id'], user.userprofile.id)
355 158
 
356 159
     def test_search_accounts(self):
357 160
         client = Client()
@@ -381,7 +184,7 @@ def test_search_skills(self):
381 184
         response = client.get(url, follow=True)
382 185
         data = json.loads(response.content)
383 186
         eq_(len(data['objects']), 1)
384  
-        eq_(data['objects'][0]['id'], unicode(user_1.userprofile.id))
  187
+        eq_(data['objects'][0]['id'], user_1.userprofile.id)
385 188
 
386 189
     def test_search_groups(self):
387 190
         client = Client()
@@ -396,7 +199,7 @@ def test_search_groups(self):
396 199
         response = client.get(url, follow=True)
397 200
         data = json.loads(response.content)
398 201
         eq_(len(data['objects']), 1)
399  
-        eq_(data['objects'][0]['id'], unicode(user_1.userprofile.id))
  202
+        eq_(data['objects'][0]['id'], user_1.userprofile.id)
400 203
 
401 204
     def test_search_combined_skills_country(self):
402 205
         country = 'fr'
@@ -410,7 +213,7 @@ def test_search_combined_skills_country(self):
410 213
         response = client.get(url, follow=True)
411 214
         data = json.loads(response.content)
412 215
         eq_(len(data['objects']), 1)
413  
-        eq_(data['objects'][0]['id'], unicode(user_1.userprofile.id))
  216
+        eq_(data['objects'][0]['id'], user_1.userprofile.id)
414 217
 
415 218
     def test_query_with_space(self):
416 219
         user = UserFactory.create(userprofile={'city': 'Mountain View'})
@@ -419,7 +222,7 @@ def test_query_with_space(self):
419 222
         request = client.get(url, follow=True)
420 223
         data = json.loads(request.content)
421 224
         eq_(len(data['objects']), 1)
422  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  225
+        eq_(data['objects'][0]['id'], user.userprofile.id)
423 226
 
424 227
     def test_search_name(self):
425 228
         user = UserFactory.create(userprofile={'full_name': u'Νίκος Κούκος'})
@@ -429,7 +232,7 @@ def test_search_name(self):
429 232
         request = client.get(url, follow=True)
430 233
         data = json.loads(request.content)
431 234
         eq_(len(data['objects']), 1)
432  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  235
+        eq_(data['objects'][0]['id'], user.userprofile.id)
433 236
 
434 237
     def test_search_username(self):
435 238
         user = UserFactory.create()
@@ -438,7 +241,7 @@ def test_search_username(self):
438 241
         response = client.get(url, follow=True)
439 242
         data = json.loads(response.content)
440 243
         eq_(len(data['objects']), 1)
441  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  244
+        eq_(data['objects'][0]['id'], user.userprofile.id)
442 245
 
443 246
     def test_search_country(self):
444 247
         user = UserFactory.create(userprofile={'country': 'fr'})
@@ -448,7 +251,7 @@ def test_search_country(self):
448 251
         response = client.get(url, follow=True)
449 252
         data = json.loads(response.content)
450 253
         eq_(len(data['objects']), 1)
451  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  254
+        eq_(data['objects'][0]['id'], user.userprofile.id)
452 255
 
453 256
     def test_search_region(self):
454 257
         user = UserFactory.create(userprofile={'region': 'la lo'})
@@ -458,7 +261,7 @@ def test_search_region(self):
458 261
         response = client.get(url, follow=True)
459 262
         data = json.loads(response.content)
460 263
         eq_(len(data['objects']), 1)
461  
-        eq_(data['objects'][0]['id'], unicode(user.userprofile.id))
  264
+        eq_(data['objects'][0]['id'], user.userprofile.id)
462 265
 
463 266
     def test_search_city(self):
464 267
         user = UserFactory.create(userprofile={'city': u'αθήνα'})
@@ -468,7 +271,7 @@ def test_search_city(self):
468 271
         response = client.get(url, follow=True)
469 272
         data = json.loads(response.content)
470 273
         eq_(len(data['objects']), 1)