From e6dc8a2e11118e6deb48af6686d19950d10c54b8 Mon Sep 17 00:00:00 2001 From: Eric Bottard Date: Wed, 21 Mar 2012 17:54:59 +0100 Subject: [PATCH] Added API for accessing directories and files. --- .../bitbucket/api/BitBucketDirectory.java | 45 ++++++ .../social/bitbucket/api/BitBucketFile.java | 40 +++++ .../bitbucket/api/BitBucketFileMetadata.java | 46 ++++++ .../social/bitbucket/api/RepoOperations.java | 14 ++ .../bitbucket/api/impl/RepoTemplate.java | 22 ++- .../bitbucket/api/impl/RepoTemplateTest.java | 56 +++++++ .../bitbucket/api/impl/repo-directories.json | 143 ++++++++++++++++++ .../social/bitbucket/api/impl/repo-file.json | 5 + 8 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/social/bitbucket/api/BitBucketDirectory.java create mode 100644 src/main/java/org/springframework/social/bitbucket/api/BitBucketFile.java create mode 100644 src/main/java/org/springframework/social/bitbucket/api/BitBucketFileMetadata.java create mode 100644 src/test/resources/org/springframework/social/bitbucket/api/impl/repo-directories.json create mode 100644 src/test/resources/org/springframework/social/bitbucket/api/impl/repo-file.json diff --git a/src/main/java/org/springframework/social/bitbucket/api/BitBucketDirectory.java b/src/main/java/org/springframework/social/bitbucket/api/BitBucketDirectory.java new file mode 100644 index 0000000..e6ece9d --- /dev/null +++ b/src/main/java/org/springframework/social/bitbucket/api/BitBucketDirectory.java @@ -0,0 +1,45 @@ +package org.springframework.social.bitbucket.api; + +import java.util.List; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * Metadata about the contents of a repository directory. Contains files, + * directories and metadata about the selected directory. + * + * @author ericbottard + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitBucketDirectory { + + @JsonProperty + private List directories; + + @JsonProperty + private List files; + + @JsonProperty + private String path; + + @JsonProperty + private String node; + + public List getDirectories() { + return directories; + } + + public List getFiles() { + return files; + } + + public String getPath() { + return path; + } + + public String getNode() { + return node; + } + +} diff --git a/src/main/java/org/springframework/social/bitbucket/api/BitBucketFile.java b/src/main/java/org/springframework/social/bitbucket/api/BitBucketFile.java new file mode 100644 index 0000000..dd6b5fc --- /dev/null +++ b/src/main/java/org/springframework/social/bitbucket/api/BitBucketFile.java @@ -0,0 +1,40 @@ +package org.springframework.social.bitbucket.api; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * Content as well as metadata about a repository file. + * + * @author ebottard + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitBucketFile { + + @JsonProperty + private String node; + + @JsonProperty + private String path; + + @JsonProperty + private String data; + + public String getNode() { + return node; + } + + /** + * Returns the file path, relative to the root of the repository. + */ + public String getPath() { + return path; + } + + /** + * Returns the actual content of the file, as a String. + */ + public String getData() { + return data; + } +} diff --git a/src/main/java/org/springframework/social/bitbucket/api/BitBucketFileMetadata.java b/src/main/java/org/springframework/social/bitbucket/api/BitBucketFileMetadata.java new file mode 100644 index 0000000..5932eff --- /dev/null +++ b/src/main/java/org/springframework/social/bitbucket/api/BitBucketFileMetadata.java @@ -0,0 +1,46 @@ +package org.springframework.social.bitbucket.api; + +import java.util.Date; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; +import org.codehaus.jackson.map.annotate.JsonDeserialize; +import org.springframework.social.bitbucket.api.impl.UTCDateDeserializer; + +/** + * Metadata about a file in a repository. + * + * @author ericbottard + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitBucketFileMetadata { + + @JsonProperty + private String path; + + @JsonProperty + private String revision; + + @JsonProperty("utctimestamp") + @JsonDeserialize(using = UTCDateDeserializer.class) + private Date timestamp; + + @JsonProperty + private int size; + + public String getPath() { + return path; + } + + public String getRevision() { + return revision; + } + + public Date getTimestamp() { + return timestamp; + } + + public int getSize() { + return size; + } +} diff --git a/src/main/java/org/springframework/social/bitbucket/api/RepoOperations.java b/src/main/java/org/springframework/social/bitbucket/api/RepoOperations.java index 0e5b97f..0e789df 100644 --- a/src/main/java/org/springframework/social/bitbucket/api/RepoOperations.java +++ b/src/main/java/org/springframework/social/bitbucket/api/RepoOperations.java @@ -64,4 +64,18 @@ public interface RepoOperations { */ public BitBucketChangesets getChangesets(String user, String repoSlug, String start, int limit); + + /** + * Returns information about a known directory, including children + * directories and files. + */ + public BitBucketDirectory getDirectory(String user, String repoSlug, + String revision, String path); + + /** + * Returns information and actual contents (as a String) about a known file + * path. + */ + public BitBucketFile getFile(String string, String string2, String string3, + String string4); } diff --git a/src/main/java/org/springframework/social/bitbucket/api/impl/RepoTemplate.java b/src/main/java/org/springframework/social/bitbucket/api/impl/RepoTemplate.java index 9dce714..f412492 100644 --- a/src/main/java/org/springframework/social/bitbucket/api/impl/RepoTemplate.java +++ b/src/main/java/org/springframework/social/bitbucket/api/impl/RepoTemplate.java @@ -23,9 +23,11 @@ import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; +import org.springframework.social.bitbucket.api.BitBucketChangeset; import org.springframework.social.bitbucket.api.BitBucketChangesets; +import org.springframework.social.bitbucket.api.BitBucketDirectory; +import org.springframework.social.bitbucket.api.BitBucketFile; import org.springframework.social.bitbucket.api.BitBucketRepository; -import org.springframework.social.bitbucket.api.BitBucketChangeset; import org.springframework.social.bitbucket.api.BitBucketUser; import org.springframework.social.bitbucket.api.RepoOperations; import org.springframework.web.client.RestTemplate; @@ -92,6 +94,24 @@ public BitBucketChangesets getChangesets(String user, String repoSlug, repoSlug, start, limit); } + @Override + public BitBucketDirectory getDirectory(String user, String repoSlug, + String revision, String path) { + return restTemplate.getForObject( + buildUrl("/repositories/{user}/{slug}/src/{rev}/{path}/") + .toString(), BitBucketDirectory.class, user, repoSlug, + revision, path); + } + + @Override + public BitBucketFile getFile(String user, String repoSlug, String revision, + String path) { + return restTemplate.getForObject( + buildUrl("/repositories/{user}/{slug}/src/{rev}/{path}") + .toString(), BitBucketFile.class, user, repoSlug, + revision, path); + } + /** * Exists for the sole purpose of having a strongly typed Map for Jackson. */ diff --git a/src/test/java/org/springframework/social/bitbucket/api/impl/RepoTemplateTest.java b/src/test/java/org/springframework/social/bitbucket/api/impl/RepoTemplateTest.java index 32d33ff..4aee85c 100644 --- a/src/test/java/org/springframework/social/bitbucket/api/impl/RepoTemplateTest.java +++ b/src/test/java/org/springframework/social/bitbucket/api/impl/RepoTemplateTest.java @@ -15,6 +15,7 @@ */ package org.springframework.social.bitbucket.api.impl; +import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.http.HttpMethod.*; import static org.springframework.test.web.client.RequestMatchers.*; @@ -30,6 +31,9 @@ import org.springframework.social.bitbucket.api.BitBucketChangeset; import org.springframework.social.bitbucket.api.BitBucketChangeset.FileModificationType; import org.springframework.social.bitbucket.api.BitBucketChangesets; +import org.springframework.social.bitbucket.api.BitBucketDirectory; +import org.springframework.social.bitbucket.api.BitBucketFile; +import org.springframework.social.bitbucket.api.BitBucketFileMetadata; import org.springframework.social.bitbucket.api.BitBucketRepository; import org.springframework.social.bitbucket.api.BitBucketSCM; import org.springframework.social.bitbucket.api.BitBucketUser; @@ -206,4 +210,56 @@ public void testRepoChangesets() { } + @Test + public void testRepoDirectoryListing() { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + + mockServer + .expect(requestTo("https://api.bitbucket.org/1.0/repositories/jespern/django-piston/src/tip/piston/")) + .andExpect(method(GET)) + .andRespond( + withResponse(jsonResource("repo-directories"), + responseHeaders)); + + BitBucketDirectory directory = bitBucket.repoOperations().getDirectory( + "jespern", "django-piston", "tip", "piston"); + + assertThat(directory.getNode(), equalTo("4fe8af1db59d")); + assertThat(directory.getPath(), equalTo("piston/")); + + assertThat(directory.getDirectories().size(), equalTo(2)); + assertThat(directory.getDirectories(), + hasItems("fixtures", "templates")); + + BitBucketFileMetadata metadata = directory.getFiles().get(0); + assertThat(metadata.getPath(), equalTo("piston/utils.py")); + assertThat(metadata.getRevision(), equalTo("112311f7d7ce")); + assertThat(metadata.getSize(), equalTo(12275)); + + } + + @Test + public void testRepoFile() { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + + mockServer + .expect(requestTo("https://api.bitbucket.org/1.0/repositories/jespern/django-piston/src/tip/piston/utils.py")) + .andExpect(method(GET)) + .andRespond( + withResponse(jsonResource("repo-file"), responseHeaders)); + + BitBucketFile file = bitBucket.repoOperations().getFile("jespern", + "django-piston", "tip", "piston/utils.py"); + + assertThat(file.getNode(), equalTo("4fe8af1db59d")); + assertThat(file.getPath(), equalTo("piston/utils.py")); + assertThat(file.getData().length(), equalTo(12275)); + // The following makes sure that newline escapes are correcly handled + assertThat( + file.getData(), + startsWith("import time\nfrom django.http import HttpResponseNotAllowed,")); + + } } diff --git a/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-directories.json b/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-directories.json new file mode 100644 index 0000000..91b58e4 --- /dev/null +++ b/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-directories.json @@ -0,0 +1,143 @@ +{ + "node": "4fe8af1db59d", + "path": "piston/", + "directories": [ + "fixtures", + "templates" + ], + "files": [ + { + "size": 12275, + "path": "piston/utils.py", + "timestamp": "2010-04-07 15:26:17", + "utctimestamp": "2010-04-07 15:26:17+00:00", + "revision": "112311f7d7ce" + }, + { + "size": 4771, + "path": "piston/models.py", + "timestamp": "2010-05-28 12:52:57", + "utctimestamp": "2010-05-28 12:52:57+00:00", + "revision": "8d3cd0663f31" + }, + { + "size": 3229, + "path": "piston/store.py", + "timestamp": "2009-09-11 09:15:31", + "utctimestamp": "2009-09-11 09:15:31+00:00", + "revision": "1b139755d6f9" + }, + { + "size": 4693, + "path": "piston/handler.py", + "timestamp": "2010-02-01 16:15:09", + "utctimestamp": "2010-02-01 16:15:09+00:00", + "revision": "4b24b73f46a1" + }, + { + "size": 314, + "path": "piston/signals.py", + "timestamp": "2009-08-04 21:15:58", + "utctimestamp": "2009-08-04 21:15:58+00:00", + "revision": "a84ccc4b421c" + }, + { + "size": 833, + "path": "piston/middleware.py", + "timestamp": "2009-05-12 10:21:08", + "utctimestamp": "2009-05-12 10:21:08+00:00", + "revision": "b0a1571ff61a" + }, + { + "size": 2064, + "path": "piston/test.py", + "timestamp": "2009-08-05 00:05:30", + "utctimestamp": "2009-08-05 00:05:30+00:00", + "revision": "4630a92d644f" + }, + { + "size": 6890, + "path": "piston/decorator.py", + "timestamp": "2009-04-24 07:25:43", + "utctimestamp": "2009-04-24 07:25:43+00:00", + "revision": "788bbd3840aa" + }, + { + "size": 2037, + "path": "piston/forms.py", + "timestamp": "2009-09-11 09:15:31", + "utctimestamp": "2009-09-11 09:15:31+00:00", + "revision": "1b139755d6f9" + }, + { + "size": 6221, + "path": "piston/validate_jsonp.py", + "timestamp": "2010-02-15 17:51:50", + "utctimestamp": "2010-02-15 17:51:50+00:00", + "revision": "f558b2c66dcc" + }, + { + "size": 7013, + "path": "piston/tests.py", + "timestamp": "2010-06-25 12:19:14", + "utctimestamp": "2010-06-25 12:19:14+00:00", + "revision": "ad479aec5b15" + }, + { + "size": 10611, + "path": "piston/resource.py", + "timestamp": "2010-07-18 11:26:09", + "utctimestamp": "2010-07-18 11:26:09+00:00", + "revision": "dc0ee00d3bfc" + }, + { + "size": 5785, + "path": "piston/doc.py", + "timestamp": "2010-06-25 09:12:10", + "utctimestamp": "2010-06-25 09:12:10+00:00", + "revision": "882d38485abc" + }, + { + "size": 1370, + "path": "piston/handlers_doc.py", + "timestamp": "2010-02-22 08:00:14", + "utctimestamp": "2010-02-22 08:00:14+00:00", + "revision": "1b3ce1ad25da" + }, + { + "size": 383, + "path": "piston/__init__.py", + "timestamp": "2010-03-04 14:14:35", + "utctimestamp": "2010-03-04 14:14:35+00:00", + "revision": "864e4c095065" + }, + { + "size": 23013, + "path": "piston/oauth.py", + "timestamp": "2009-09-08 10:49:43", + "utctimestamp": "2009-09-08 10:49:43+00:00", + "revision": "21a24da68710" + }, + { + "size": 15564, + "path": "piston/emitters.py", + "timestamp": "2011-11-01 13:51:53", + "utctimestamp": "2011-11-01 13:51:53+00:00", + "revision": "24fcc3586459" + }, + { + "size": 2009, + "path": "piston/managers.py", + "timestamp": "2009-06-06 19:03:03", + "utctimestamp": "2009-06-06 19:03:03+00:00", + "revision": "8a965327e57a" + }, + { + "size": 10751, + "path": "piston/authentication.py", + "timestamp": "2010-01-11 16:02:33", + "utctimestamp": "2010-01-11 16:02:33+00:00", + "revision": "4874292b3f48" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-file.json b/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-file.json new file mode 100644 index 0000000..bf7da69 --- /dev/null +++ b/src/test/resources/org/springframework/social/bitbucket/api/impl/repo-file.json @@ -0,0 +1,5 @@ +{ + "node": "4fe8af1db59d", + "path": "piston/utils.py", + "data": "import time\nfrom django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest\nfrom django.core.urlresolvers import reverse\nfrom django.core.cache import cache\nfrom django import get_version as django_version\nfrom django.core.mail import send_mail, mail_admins\nfrom django.conf import settings\nfrom django.utils.translation import ugettext as _\nfrom django.template import loader, TemplateDoesNotExist\nfrom django.contrib.sites.models import Site\nfrom decorator import decorator\n\nfrom datetime import datetime, timedelta\n\n__version__ = '0.2.3rc1'\n\ndef get_version():\n return __version__\n\ndef format_error(error):\n return u\"Piston/%s (Django %s) crash report:\\n\\n%s\" % \\\n (get_version(), django_version(), error)\n\nclass rc_factory(object):\n \"\"\"\n Status codes.\n \"\"\"\n CODES = dict(ALL_OK = ('OK', 200),\n CREATED = ('Created', 201),\n DELETED = ('', 204), # 204 says \"Don't send a body!\"\n BAD_REQUEST = ('Bad Request', 400),\n FORBIDDEN = ('Forbidden', 401),\n NOT_FOUND = ('Not Found', 404),\n DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),\n NOT_HERE = ('Gone', 410),\n INTERNAL_ERROR = ('Internal Error', 500),\n NOT_IMPLEMENTED = ('Not Implemented', 501),\n THROTTLED = ('Throttled', 503))\n\n def __getattr__(self, attr):\n \"\"\"\n Returns a fresh `HttpResponse` when getting \n an \"attribute\". This is backwards compatible\n with 0.2, which is important.\n \"\"\"\n try:\n (r, c) = self.CODES.get(attr)\n except TypeError:\n raise AttributeError(attr)\n\n class HttpResponseWrapper(HttpResponse):\n \"\"\"\n Wrap HttpResponse and make sure that the internal _is_string \n flag is updated when the _set_content method (via the content \n property) is called\n \"\"\"\n def _set_content(self, content):\n \"\"\"\n Set the _container and _is_string properties based on the \n type of the value parameter. This logic is in the construtor\n for HttpResponse, but doesn't get repeated when setting \n HttpResponse.content although this bug report (feature request)\n suggests that it should: http://code.djangoproject.com/ticket/9403 \n \"\"\"\n if not isinstance(content, basestring) and hasattr(content, '__iter__'):\n self._container = content\n self._is_string = False\n else:\n self._container = [content]\n self._is_string = True\n\n content = property(HttpResponse._get_content, _set_content) \n\n return HttpResponseWrapper(r, content_type='text/plain', status=c)\n \nrc = rc_factory()\n \nclass FormValidationError(Exception):\n def __init__(self, form):\n self.form = form\n\nclass HttpStatusCode(Exception):\n def __init__(self, response):\n self.response = response\n\ndef validate(v_form, operation='POST'):\n @decorator\n def wrap(f, self, request, *a, **kwa):\n form = v_form(getattr(request, operation))\n \n if form.is_valid():\n setattr(request, 'form', form)\n return f(self, request, *a, **kwa)\n else:\n raise FormValidationError(form)\n return wrap\n\ndef throttle(max_requests, timeout=60*60, extra=''):\n \"\"\"\n Simple throttling decorator, caches\n the amount of requests made in cache.\n \n If used on a view where users are required to\n log in, the username is used, otherwise the\n IP address of the originating request is used.\n \n Parameters::\n - `max_requests`: The maximum number of requests\n - `timeout`: The timeout for the cache entry (default: 1 hour)\n \"\"\"\n @decorator\n def wrap(f, self, request, *args, **kwargs):\n if request.user.is_authenticated():\n ident = request.user.username\n else:\n ident = request.META.get('REMOTE_ADDR', None)\n \n if hasattr(request, 'throttle_extra'):\n \"\"\"\n Since we want to be able to throttle on a per-\n application basis, it's important that we realize\n that `throttle_extra` might be set on the request\n object. If so, append the identifier name with it.\n \"\"\"\n ident += ':%s' % str(request.throttle_extra)\n \n if ident:\n \"\"\"\n Preferrably we'd use incr/decr here, since they're\n atomic in memcached, but it's in django-trunk so we\n can't use it yet. If someone sees this after it's in\n stable, you can change it here.\n \"\"\"\n ident += ':%s' % extra\n \n now = time.time()\n count, expiration = cache.get(ident, (1, None))\n\n if expiration is None:\n expiration = now + timeout\n\n if count >= max_requests and expiration > now:\n t = rc.THROTTLED\n wait = int(expiration - now)\n t.content = 'Throttled, wait %d seconds.' % wait\n t['Retry-After'] = wait\n return t\n\n cache.set(ident, (count+1, expiration), (expiration - now))\n \n return f(self, request, *args, **kwargs)\n return wrap\n\ndef coerce_put_post(request):\n \"\"\"\n Django doesn't particularly understand REST.\n In case we send data over PUT, Django won't\n actually look at the data and load it. We need\n to twist its arm here.\n \n The try/except abominiation here is due to a bug\n in mod_python. This should fix it.\n \"\"\"\n if request.method == \"PUT\":\n # Bug fix: if _load_post_and_files has already been called, for\n # example by middleware accessing request.POST, the below code to\n # pretend the request is a POST instead of a PUT will be too late\n # to make a difference. Also calling _load_post_and_files will result \n # in the following exception:\n # AttributeError: You cannot set the upload handlers after the upload has been processed.\n # The fix is to check for the presence of the _post field which is set \n # the first time _load_post_and_files is called (both by wsgi.py and \n # modpython.py). If it's set, the request has to be 'reset' to redo\n # the query value parsing in POST mode.\n if hasattr(request, '_post'):\n del request._post\n del request._files\n \n try:\n request.method = \"POST\"\n request._load_post_and_files()\n request.method = \"PUT\"\n except AttributeError:\n request.META['REQUEST_METHOD'] = 'POST'\n request._load_post_and_files()\n request.META['REQUEST_METHOD'] = 'PUT'\n \n request.PUT = request.POST\n\n\nclass MimerDataException(Exception):\n \"\"\"\n Raised if the content_type and data don't match\n \"\"\"\n pass\n\nclass Mimer(object):\n TYPES = dict()\n \n def __init__(self, request):\n self.request = request\n \n def is_multipart(self):\n content_type = self.content_type()\n\n if content_type is not None:\n return content_type.lstrip().startswith('multipart')\n\n return False\n\n def loader_for_type(self, ctype):\n \"\"\"\n Gets a function ref to deserialize content\n for a certain mimetype.\n \"\"\"\n for loadee, mimes in Mimer.TYPES.iteritems():\n for mime in mimes:\n if ctype.startswith(mime):\n return loadee\n \n def content_type(self):\n \"\"\"\n Returns the content type of the request in all cases where it is\n different than a submitted form - application/x-www-form-urlencoded\n \"\"\"\n type_formencoded = \"application/x-www-form-urlencoded\"\n\n ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)\n \n if type_formencoded in ctype:\n return None\n \n return ctype\n\n def translate(self):\n \"\"\"\n Will look at the `Content-type` sent by the client, and maybe\n deserialize the contents into the format they sent. This will\n work for JSON, YAML, XML and Pickle. Since the data is not just\n key-value (and maybe just a list), the data will be placed on\n `request.data` instead, and the handler will have to read from\n there.\n \n It will also set `request.content_type` so the handler has an easy\n way to tell what's going on. `request.content_type` will always be\n None for form-encoded and/or multipart form data (what your browser sends.)\n \"\"\" \n ctype = self.content_type()\n self.request.content_type = ctype\n \n if not self.is_multipart() and ctype:\n loadee = self.loader_for_type(ctype)\n \n if loadee:\n try:\n self.request.data = loadee(self.request.raw_post_data)\n \n # Reset both POST and PUT from request, as its\n # misleading having their presence around.\n self.request.POST = self.request.PUT = dict()\n except (TypeError, ValueError):\n # This also catches if loadee is None.\n raise MimerDataException\n else:\n self.request.data = None\n\n return self.request\n \n @classmethod\n def register(cls, loadee, types):\n cls.TYPES[loadee] = types\n \n @classmethod\n def unregister(cls, loadee):\n return cls.TYPES.pop(loadee)\n\ndef translate_mime(request):\n request = Mimer(request).translate()\n \ndef require_mime(*mimes):\n \"\"\"\n Decorator requiring a certain mimetype. There's a nifty\n helper called `require_extended` below which requires everything\n we support except for post-data via form.\n \"\"\"\n @decorator\n def wrap(f, self, request, *args, **kwargs):\n m = Mimer(request)\n realmimes = set()\n\n rewrite = { 'json': 'application/json',\n 'yaml': 'application/x-yaml',\n 'xml': 'text/xml',\n 'pickle': 'application/python-pickle' }\n\n for idx, mime in enumerate(mimes):\n realmimes.add(rewrite.get(mime, mime))\n\n if not m.content_type() in realmimes:\n return rc.BAD_REQUEST\n\n return f(self, request, *args, **kwargs)\n return wrap\n\nrequire_extended = require_mime('json', 'yaml', 'xml', 'pickle')\n \ndef send_consumer_mail(consumer):\n \"\"\"\n Send a consumer an email depending on what their status is.\n \"\"\"\n try:\n subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]\n except AttributeError:\n subject = \"Your API Consumer for %s \" % Site.objects.get_current().name\n if consumer.status == \"accepted\":\n subject += \"was accepted!\"\n elif consumer.status == \"canceled\":\n subject += \"has been canceled.\"\n elif consumer.status == \"rejected\":\n subject += \"has been rejected.\"\n else: \n subject += \"is awaiting approval.\"\n\n template = \"piston/mails/consumer_%s.txt\" % consumer.status \n \n try:\n body = loader.render_to_string(template, \n { 'consumer' : consumer, 'user' : consumer.user })\n except TemplateDoesNotExist:\n \"\"\" \n They haven't set up the templates, which means they might not want\n these emails sent.\n \"\"\"\n return \n\n try:\n sender = settings.PISTON_FROM_EMAIL\n except AttributeError:\n sender = settings.DEFAULT_FROM_EMAIL\n\n if consumer.user:\n send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)\n\n if consumer.status == 'pending' and len(settings.ADMINS):\n mail_admins(_(subject), body, fail_silently=True)\n\n if settings.DEBUG and consumer.user:\n print \"Mail being sent, to=%s\" % consumer.user.email\n print \"Subject: %s\" % _(subject)\n print body\n\n" +} \ No newline at end of file