@allowed_methods to check for correct HTTP method
[mygpo.git] / mygpo / api / simple.py
blobd396967de200c239d2d383ab0e56f941cdef4f98
2 # This file is part of my.gpodder.org.
4 # my.gpodder.org is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
9 # my.gpodder.org is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12 # License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
18 from mygpo.api.basic_auth import require_valid_user, check_username
19 from django.http import HttpResponse, HttpResponseBadRequest
20 from mygpo.api.models import Device, SubscriptionAction, Podcast, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, SuggestionEntry
21 from mygpo.api.opml import Exporter, Importer
22 from mygpo.api.httpresponse import JsonResponse
23 from mygpo.api.sanitizing import sanitize_url
24 from mygpo.api.backend import get_toplist, get_all_subscriptions
25 from django.views.decorators.csrf import csrf_exempt
26 from django.shortcuts import get_object_or_404
27 from mygpo.search.models import SearchEntry
28 from django.utils.translation import ugettext as _
29 from mygpo.decorators import allowed_methods
32 try:
33 import json
35 # Python 2.5 seems to have a different json module
36 if not 'dumps' in dir(json):
37 raise ImportError
39 except ImportError:
40 import simplejson as json
43 ALLOWED_FORMATS = ('txt', 'opml', 'json')
45 def check_format(fn):
46 def tmp(request, format, *args, **kwargs):
47 if not format in ALLOWED_FORMATS:
48 return HttpResponseBadRequest('Invalid format')
50 return fn(request, *args, format=format, **kwargs)
51 return tmp
54 @csrf_exempt
55 @require_valid_user
56 @check_username
57 @check_format
58 @allowed_methods(['GET', 'PUT', 'POST'])
59 def subscriptions(request, username, device_uid, format):
61 if request.method == 'GET':
62 title = _('%(username)s\'s Subscription List') % {'username': username}
63 subscriptions = get_subscriptions(request.user, device_uid)
64 return format_podcast_list(subscriptions, format, title)
66 elif request.method in ('PUT', 'POST'):
67 subscriptions = parse_subscription(request.raw_post_data, format)
68 return set_subscriptions(subscriptions, request.user, device_uid)
71 @csrf_exempt
72 @require_valid_user
73 @check_username
74 @check_format
75 @allowed_methods(['GET'])
76 def all_subscriptions(request, username, format):
77 subscriptions = get_all_subscriptions(request.user)
78 title = _('%(username)s\'s Subscription List') % {'username': username}
79 return format_podcast_list(subscriptions, format, title)
82 def format_podcast_list(obj_list, format, title, get_podcast=lambda x: x, json_map=lambda x: x.url):
83 """
84 Formats a list of podcasts for use in a API response
86 obj_list is a list of podcasts or objects that contain podcasts
87 format is one if txt, opml or json
88 title is a label of the list
89 if obj_list is a list of objects containing podcasts, get_podcast is the
90 function used to get the podcast out of the each of these objects
91 json_map is a function returning the contents of an object (from obj_list)
92 that should be contained in the result (only used for format='json')
93 """
94 if format == 'txt':
95 podcasts = map(get_podcast, obj_list)
96 s = '\n'.join([p.url for p in podcasts] + [''])
97 return HttpResponse(s, mimetype='text/plain')
99 elif format == 'opml':
100 podcasts = map(get_podcast, obj_list)
101 exporter = Exporter(title)
102 opml = exporter.generate(podcasts)
103 return HttpResponse(opml, mimetype='text/xml')
105 elif format == 'json':
106 objs = map(json_map, obj_list)
107 return JsonResponse(objs)
109 else:
110 return None
113 def get_subscriptions(user, device_uid):
114 device = get_object_or_404(Device, uid=device_uid, user=user, deleted=False)
115 return [s.podcast for s in device.get_subscriptions()]
118 def parse_subscription(raw_post_data, format):
119 if format == 'txt':
120 urls = raw_post_data.split('\n')
122 elif format == 'opml':
123 begin = raw_post_data.find('<?xml')
124 end = raw_post_data.find('</opml>') + 7
125 i = Importer(content=raw_post_data[begin:end])
126 urls = [p['url'] for p in i.items]
128 elif format == 'json':
129 begin = raw_post_data.find('[')
130 end = raw_post_data.find(']') + 1
131 urls = json.loads(raw_post_data[begin:end])
133 else:
134 return []
136 urls = map(sanitize_url, urls)
137 urls = filter(lambda x: x, urls)
138 urls = set(urls)
139 return urls
142 def set_subscriptions(urls, user, device_uid):
143 device, created = Device.objects.get_or_create(user=user, uid=device_uid,
144 defaults = {'type': 'other', 'name': device_uid})
146 # undelete a previously deleted device
147 if device.deleted:
148 device.deleted = False
149 device.save()
151 old = [s.podcast.url for s in device.get_subscriptions()]
152 new = [p for p in urls if p not in old]
153 rem = [p for p in old if p not in urls]
155 for r in rem:
156 p = Podcast.objects.get(url=r)
157 s = SubscriptionAction(podcast=p, device=device, action=UNSUBSCRIBE_ACTION)
158 s.save()
160 for n in new:
161 p, created = Podcast.objects.get_or_create(url=n)
162 s = SubscriptionAction(podcast=p, action=SUBSCRIBE_ACTION, device=device)
163 s.save()
165 # Only an empty response is a successful response
166 return HttpResponse('', mimetype='text/plain')
169 @check_format
170 @allowed_methods(['GET'])
171 def toplist(request, count, format):
172 if int(count) not in range(1,100):
173 count = 100
175 toplist = get_toplist(count)
176 json_map = lambda t: {'url': t.get_podcast().url,
177 'title':t.get_podcast().title,
178 'description':t.get_podcast().description,
179 'subscribers':t.subscriptions,
180 'subscribers_last_week':t.oldplace}
181 title = _('gpodder.net - Top %(count)d') % {'count': len(toplist)}
182 return format_podcast_list(toplist,
183 format,
184 title,
185 get_podcast=lambda x: x.get_podcast(),
186 json_map=json_map)
189 @check_format
190 @allowed_methods(['GET'])
191 def search(request, format):
192 query = request.GET.get('q', '').encode('utf-8')
194 if not query:
195 return HttpResponseBadRequest('/search.opml|txt|json?q={query}')
197 results = [r.get_podcast() for r in SearchEntry.objects.search(query)[:20]]
199 json_map = lambda p: {'url':p.url, 'title':p.title, 'description':p.description}
200 title = _('gpodder.net - Search')
201 return format_podcast_list(results, format, title, json_map=json_map)
204 @require_valid_user
205 @check_format
206 @allowed_methods(['GET'])
207 def suggestions(request, count, format):
208 if int(count) not in range(1,100):
209 count = 100
211 suggestions = SuggestionEntry.objects.for_user(user)[:int(count)]
212 json_map = lambda p: {'url': p.url, 'title': p.title, 'description': p.description}
213 title = _('gpodder.net - %(count)d Suggestions') % {'count': len(suggestions)}
214 return format_podcast_list(suggestions, format, title, json_map=json_map)