我发现 Tastypie 创建捆绑对象的方式存在一些问题:
1) 当我在不包含“id”字段的情况下对 Card 资源的详细端点(见下文 tests.py)进行 PUT 操作时,不是从 URI 更新资源,而是创建了一个新资源。
2)另一个更重要的问题是,如果我包含“id”字段,则“id”中指示的资源会更新,而不是 URI 上的资源;这意味着我可以通过指向任何其他详细 URI 来更新任何资源(我还没有内置 auth,但这可能是未来的授权问题)。
下面的第一个测试失败(除非我在模型字段定义中删除 default=uuid1_as_base64 并在模型的保存方法中实现逻辑),下面的第二个测试通过,即使它不应该通过。
这里发生了什么?
测试.py
def test_PUT_detail(self):
# Test 1: PUT to detail endpoint of card 1 without an ID field
put_data = {'text': 'wakka wakka'}
response = self.api_client.put(
uri='/api/v1/cards/rpHBJNOkEeKr2hTa6Uod1w/', # card 1 uri
data=put_data
)
self.assertEqual(self.card_1.read_file(), 'wakka wakka')
# Test 2: PUT to detail endpoint of card 1 with ID field of card 2
put_data = {'id': 'twt_UtOkEeKsuxTa6Uod1w', 'text': 'wakka wakka'}
response = self.api_client.put(
uri='/api/v1/cards/rpHBJNOkEeKr2hTa6Uod1w/', # still card 1 uri
data=put_data
)
self.assertEqual(self.card_2.read_file(), 'wakka wakka')
在上述测试期间创建的捆绑对象:
#Test 1
<Bundle for obj: 'rpHBJNOkEeKr2hTa6Uod1w' and with data: '{'text': u'wakka wakka', 'pk': u'rpHBJNOkEeKr2hTa6Uod1w'}'>
#Test 2 (notice how both the id field and pk field are included)
<Bundle for obj: 'twt_UtOkEeKsuxTa6Uod1w' and with data: '{'text': u'wakka wakka', 'id': u'twt_UtOkEeKsuxTa6Uod1w', 'pk': u'rpHBJNOkEeKr2hTa6Uod1w'}'>
api.py
class CardResource(ModelResource):
text = fields.CharField()
def __init__(self, *args, **kwargs):
super(CardResource, self).__init__(*args, **kwargs)
self.fields['id'].read_only = True
class Meta:
queryset = Card.objects.all()
fields = ['id', 'text']
resource_name = 'cards'
list_allowed_methods = ['get', 'post']
detail_allowed_methods = ['get', 'put', 'delete']
authorization = Authorization()
validation = CardValidation()
include_resource_uri = False
def dehydrate_text(self, bundle):
return bundle.obj.read_file()
def save(self, *args, **kwargs):
bundle = super(CardResource, self).save(*args, **kwargs)
bundle.obj.write_file(bundle.data['text'])
return bundle
模型.py
class Card(TimeStampedModel):
id = models.CharField(max_length=22, db_index=True, primary_key=True,
default=uuid1_as_base64) # default is a callable
def file_path(self):
return '/'.join([settings.CARD_ROOT, self.id + '.txt'])
def read_file(self):
try:
with open(self.file_path(), 'rb') as f:
content = f.read()
return content
except IOError:
self.write_file(text='')
return self.read_file()
def write_file(self, text=''):
with open(self.file_path(), 'wb') as f:
f.write(text)
def __unicode__(self):
return u'%s' % self.id