mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-14 13:46:59 +03:00
FileField and ImageField
This commit is contained in:
parent
ac71d9aaae
commit
dee3f78cb6
|
@ -13,4 +13,3 @@ django-filter>=0.5.4
|
||||||
django-oauth-plus>=2.2.1
|
django-oauth-plus>=2.2.1
|
||||||
oauth2>=1.5.211
|
oauth2>=1.5.211
|
||||||
django-oauth2-provider>=0.2.4
|
django-oauth2-provider>=0.2.4
|
||||||
Pillow==2.3.0
|
|
||||||
|
|
|
@ -84,15 +84,6 @@ except ImportError:
|
||||||
from collections import UserDict
|
from collections import UserDict
|
||||||
from collections import MutableMapping as DictMixin
|
from collections import MutableMapping as DictMixin
|
||||||
|
|
||||||
# Try to import PIL in either of the two ways it can end up installed.
|
|
||||||
try:
|
|
||||||
from PIL import Image
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import Image
|
|
||||||
except ImportError:
|
|
||||||
Image = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_model_name(model_cls):
|
def get_model_name(model_cls):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -427,8 +428,6 @@ class CharField(Field):
|
||||||
return str(data)
|
return str(data)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -446,8 +445,6 @@ class EmailField(CharField):
|
||||||
return str(data).strip()
|
return str(data).strip()
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ -513,8 +510,6 @@ class IntegerField(Field):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -543,8 +538,6 @@ class FloatField(Field):
|
||||||
self.fail('invalid')
|
self.fail('invalid')
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return float(value)
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -616,9 +609,6 @@ class DecimalField(Field):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value in (None, ''):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not isinstance(value, decimal.Decimal):
|
if not isinstance(value, decimal.Decimal):
|
||||||
value = decimal.Decimal(str(value).strip())
|
value = decimal.Decimal(str(value).strip())
|
||||||
|
|
||||||
|
@ -689,7 +679,7 @@ class DateTimeField(Field):
|
||||||
self.fail('invalid', format=humanized_format)
|
self.fail('invalid', format=humanized_format)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None or self.format is None:
|
if self.format is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if self.format.lower() == ISO_8601:
|
if self.format.lower() == ISO_8601:
|
||||||
|
@ -741,7 +731,7 @@ class DateField(Field):
|
||||||
self.fail('invalid', format=humanized_format)
|
self.fail('invalid', format=humanized_format)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None or self.format is None:
|
if self.format is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Applying a `DateField` to a datetime value is almost always
|
# Applying a `DateField` to a datetime value is almost always
|
||||||
|
@ -795,7 +785,7 @@ class TimeField(Field):
|
||||||
self.fail('invalid', format=humanized_format)
|
self.fail('invalid', format=humanized_format)
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
if value is None or self.format is None:
|
if self.format is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Applying a `TimeField` to a datetime value is almost always
|
# Applying a `TimeField` to a datetime value is almost always
|
||||||
|
@ -875,14 +865,68 @@ class MultipleChoiceField(ChoiceField):
|
||||||
# File types...
|
# File types...
|
||||||
|
|
||||||
class FileField(Field):
|
class FileField(Field):
|
||||||
pass # TODO
|
default_error_messages = {
|
||||||
|
'required': _("No file was submitted."),
|
||||||
|
'invalid': _("The submitted data was not a file. Check the encoding type on the form."),
|
||||||
|
'no_name': _("No filename could be determined."),
|
||||||
|
'empty': _("The submitted file is empty."),
|
||||||
|
'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
|
||||||
|
}
|
||||||
|
use_url = api_settings.UPLOADED_FILES_USE_URL
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.max_length = kwargs.pop('max_length', None)
|
||||||
|
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
|
||||||
|
self.use_url = kwargs.pop('use_url', self.use_url)
|
||||||
|
super(FileField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
|
# `UploadedFile` objects should have name and size attributes.
|
||||||
|
file_name = data.name
|
||||||
|
file_size = data.size
|
||||||
|
except AttributeError:
|
||||||
|
self.fail('invalid')
|
||||||
|
|
||||||
|
if not file_name:
|
||||||
|
self.fail('no_name')
|
||||||
|
if not self.allow_empty_file and not file_size:
|
||||||
|
self.fail('empty')
|
||||||
|
if self.max_length and len(file_name) > self.max_length:
|
||||||
|
self.fail('max_length', max_length=self.max_length, length=len(file_name))
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if self.use_url:
|
||||||
|
return settings.MEDIA_URL + value.url
|
||||||
|
return value.name
|
||||||
|
|
||||||
|
|
||||||
class ImageField(Field):
|
class ImageField(FileField):
|
||||||
pass # TODO
|
default_error_messages = {
|
||||||
|
'invalid_image': _(
|
||||||
|
'Upload a valid image. The file you uploaded was either not an '
|
||||||
|
'image or a corrupted image.'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField)
|
||||||
|
super(ImageField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
# Image validation is a bit grungy, so we'll just outright
|
||||||
|
# defer to Django's implementation so we don't need to
|
||||||
|
# consider it, or treat PIL as a test dependancy.
|
||||||
|
file_object = super(ImageField, self).to_internal_value(data)
|
||||||
|
django_field = self._DjangoImageField()
|
||||||
|
django_field.error_messages = self.error_messages
|
||||||
|
django_field.to_python(file_object)
|
||||||
|
return file_object
|
||||||
|
|
||||||
|
|
||||||
# Advanced field types...
|
# Composite field types...
|
||||||
|
|
||||||
class ListField(Field):
|
class ListField(Field):
|
||||||
child = None
|
child = None
|
||||||
|
@ -922,6 +966,8 @@ class ListField(Field):
|
||||||
return [self.child.to_representation(item) for item in data]
|
return [self.child.to_representation(item) for item in data]
|
||||||
|
|
||||||
|
|
||||||
|
# Miscellaneous field types...
|
||||||
|
|
||||||
class ReadOnlyField(Field):
|
class ReadOnlyField(Field):
|
||||||
"""
|
"""
|
||||||
A read-only field that simply returns the field value.
|
A read-only field that simply returns the field value.
|
||||||
|
|
|
@ -110,7 +110,8 @@ DEFAULTS = {
|
||||||
# Encoding
|
# Encoding
|
||||||
'UNICODE_JSON': True,
|
'UNICODE_JSON': True,
|
||||||
'COMPACT_JSON': True,
|
'COMPACT_JSON': True,
|
||||||
'COERCE_DECIMAL_TO_STRING': True
|
'COERCE_DECIMAL_TO_STRING': True,
|
||||||
|
'UPLOADED_FILES_USE_URL': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import fields, serializers
|
from rest_framework import fields, serializers
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -516,7 +517,7 @@ class TestDecimalField(FieldValues):
|
||||||
Decimal('1.0'): '1.0',
|
Decimal('1.0'): '1.0',
|
||||||
Decimal('0.0'): '0.0',
|
Decimal('0.0'): '0.0',
|
||||||
Decimal('1.09'): '1.1',
|
Decimal('1.09'): '1.1',
|
||||||
Decimal('0.04'): '0.0',
|
Decimal('0.04'): '0.0'
|
||||||
}
|
}
|
||||||
field = fields.DecimalField(max_digits=3, decimal_places=1)
|
field = fields.DecimalField(max_digits=3, decimal_places=1)
|
||||||
|
|
||||||
|
@ -576,7 +577,7 @@ class TestDateField(FieldValues):
|
||||||
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
|
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
|
||||||
}
|
}
|
||||||
outputs = {
|
outputs = {
|
||||||
datetime.date(2001, 1, 1): '2001-01-01',
|
datetime.date(2001, 1, 1): '2001-01-01'
|
||||||
}
|
}
|
||||||
field = fields.DateField()
|
field = fields.DateField()
|
||||||
|
|
||||||
|
@ -639,7 +640,7 @@ class TestDateTimeField(FieldValues):
|
||||||
}
|
}
|
||||||
outputs = {
|
outputs = {
|
||||||
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
|
||||||
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
|
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z'
|
||||||
}
|
}
|
||||||
field = fields.DateTimeField(default_timezone=timezone.UTC())
|
field = fields.DateTimeField(default_timezone=timezone.UTC())
|
||||||
|
|
||||||
|
@ -847,6 +848,92 @@ class TestMultipleChoiceField(FieldValues):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# File fields...
|
||||||
|
|
||||||
|
class MockFile:
|
||||||
|
def __init__(self, name='', size=0, url=''):
|
||||||
|
self.name = name
|
||||||
|
self.size = size
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, MockFile) and
|
||||||
|
self.name == other.name and
|
||||||
|
self.size == other.size and
|
||||||
|
self.url == other.url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileField(FieldValues):
|
||||||
|
"""
|
||||||
|
Values for `FileField`.
|
||||||
|
"""
|
||||||
|
valid_inputs = [
|
||||||
|
(MockFile(name='example', size=10), MockFile(name='example', size=10))
|
||||||
|
]
|
||||||
|
invalid_inputs = [
|
||||||
|
('invalid', ['The submitted data was not a file. Check the encoding type on the form.']),
|
||||||
|
(MockFile(name='example.txt', size=0), ['The submitted file is empty.']),
|
||||||
|
(MockFile(name='', size=10), ['No filename could be determined.']),
|
||||||
|
(MockFile(name='x' * 100, size=10), ['Ensure this filename has at most 10 characters (it has 100).'])
|
||||||
|
]
|
||||||
|
outputs = [
|
||||||
|
(MockFile(name='example.txt', url='/example.txt'), '/example.txt')
|
||||||
|
]
|
||||||
|
field = fields.FileField(max_length=10)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFieldFieldWithName(FieldValues):
|
||||||
|
"""
|
||||||
|
Values for `FileField` with a filename output instead of URLs.
|
||||||
|
"""
|
||||||
|
valid_inputs = {}
|
||||||
|
invalid_inputs = {}
|
||||||
|
outputs = [
|
||||||
|
(MockFile(name='example.txt', url='/example.txt'), 'example.txt')
|
||||||
|
]
|
||||||
|
field = fields.FileField(use_url=False)
|
||||||
|
|
||||||
|
|
||||||
|
# Stub out mock Django `forms.ImageField` class so we don't *actually*
|
||||||
|
# call into it's regular validation, or require PIL for testing.
|
||||||
|
class FailImageValidation(object):
|
||||||
|
def to_python(self, value):
|
||||||
|
raise ValidationError(self.error_messages['invalid_image'])
|
||||||
|
|
||||||
|
|
||||||
|
class PassImageValidation(object):
|
||||||
|
def to_python(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvalidImageField(FieldValues):
|
||||||
|
"""
|
||||||
|
Values for an invalid `ImageField`.
|
||||||
|
"""
|
||||||
|
valid_inputs = {}
|
||||||
|
invalid_inputs = [
|
||||||
|
(MockFile(name='example.txt', size=10), ['Upload a valid image. The file you uploaded was either not an image or a corrupted image.'])
|
||||||
|
]
|
||||||
|
outputs = {}
|
||||||
|
field = fields.ImageField(_DjangoImageField=FailImageValidation)
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidImageField(FieldValues):
|
||||||
|
"""
|
||||||
|
Values for an valid `ImageField`.
|
||||||
|
"""
|
||||||
|
valid_inputs = [
|
||||||
|
(MockFile(name='example.txt', size=10), MockFile(name='example.txt', size=10))
|
||||||
|
]
|
||||||
|
invalid_inputs = {}
|
||||||
|
outputs = {}
|
||||||
|
field = fields.ImageField(_DjangoImageField=PassImageValidation)
|
||||||
|
|
||||||
|
|
||||||
|
# Composite fields...
|
||||||
|
|
||||||
class TestListField(FieldValues):
|
class TestListField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Values for `ListField`.
|
Values for `ListField`.
|
||||||
|
|
16
tox.ini
16
tox.ini
|
@ -21,7 +21,6 @@ basepython = python3.4
|
||||||
deps = Django==1.7
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.7]
|
[testenv:py3.3-django1.7]
|
||||||
|
@ -29,7 +28,6 @@ basepython = python3.3
|
||||||
deps = Django==1.7
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.2-django1.7]
|
[testenv:py3.2-django1.7]
|
||||||
|
@ -37,7 +35,6 @@ basepython = python3.2
|
||||||
deps = Django==1.7
|
deps = Django==1.7
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.7-django1.7]
|
[testenv:py2.7-django1.7]
|
||||||
|
@ -49,7 +46,6 @@ deps = Django==1.7
|
||||||
# oauth2==1.5.211
|
# oauth2==1.5.211
|
||||||
# django-oauth2-provider==0.2.4
|
# django-oauth2-provider==0.2.4
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.4-django1.6]
|
[testenv:py3.4-django1.6]
|
||||||
|
@ -57,7 +53,6 @@ basepython = python3.4
|
||||||
deps = Django==1.6.3
|
deps = Django==1.6.3
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.6]
|
[testenv:py3.3-django1.6]
|
||||||
|
@ -65,7 +60,6 @@ basepython = python3.3
|
||||||
deps = Django==1.6.3
|
deps = Django==1.6.3
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.2-django1.6]
|
[testenv:py3.2-django1.6]
|
||||||
|
@ -73,7 +67,6 @@ basepython = python3.2
|
||||||
deps = Django==1.6.3
|
deps = Django==1.6.3
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.7-django1.6]
|
[testenv:py2.7-django1.6]
|
||||||
|
@ -85,7 +78,6 @@ deps = Django==1.6.3
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.4
|
django-oauth2-provider==0.2.4
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.6-django1.6]
|
[testenv:py2.6-django1.6]
|
||||||
|
@ -97,7 +89,6 @@ deps = Django==1.6.3
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.4
|
django-oauth2-provider==0.2.4
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.4-django1.5]
|
[testenv:py3.4-django1.5]
|
||||||
|
@ -105,7 +96,6 @@ basepython = python3.4
|
||||||
deps = django==1.5.6
|
deps = django==1.5.6
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.3-django1.5]
|
[testenv:py3.3-django1.5]
|
||||||
|
@ -113,7 +103,6 @@ basepython = python3.3
|
||||||
deps = django==1.5.6
|
deps = django==1.5.6
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py3.2-django1.5]
|
[testenv:py3.2-django1.5]
|
||||||
|
@ -121,7 +110,6 @@ basepython = python3.2
|
||||||
deps = django==1.5.6
|
deps = django==1.5.6
|
||||||
django-filter==0.7
|
django-filter==0.7
|
||||||
defusedxml==0.3
|
defusedxml==0.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.7-django1.5]
|
[testenv:py2.7-django1.5]
|
||||||
|
@ -133,7 +121,6 @@ deps = django==1.5.6
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.3
|
django-oauth2-provider==0.2.3
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.6-django1.5]
|
[testenv:py2.6-django1.5]
|
||||||
|
@ -145,7 +132,6 @@ deps = django==1.5.6
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.3
|
django-oauth2-provider==0.2.3
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.7-django1.4]
|
[testenv:py2.7-django1.4]
|
||||||
|
@ -157,7 +143,6 @@ deps = django==1.4.11
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.3
|
django-oauth2-provider==0.2.3
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
||||||
[testenv:py2.6-django1.4]
|
[testenv:py2.6-django1.4]
|
||||||
|
@ -169,5 +154,4 @@ deps = django==1.4.11
|
||||||
oauth2==1.5.211
|
oauth2==1.5.211
|
||||||
django-oauth2-provider==0.2.3
|
django-oauth2-provider==0.2.3
|
||||||
django-guardian==1.2.3
|
django-guardian==1.2.3
|
||||||
Pillow==2.3.0
|
|
||||||
pytest-django==2.6.1
|
pytest-django==2.6.1
|
||||||
|
|
Loading…
Reference in New Issue
Block a user