我有一个Nuxt.js前端,用于使用Flask API。保存数据时我需要上传多个文件,但出现400错误,没有其他提示。
这是我的上传功能:
import os
from flask import request, json, Response, Blueprint, g
from werkzeug.utils import secure_filename
from ..models.ListingModel import ListingModel, ListingSchema
from ..models.CategoryModel import CategoryModel, CategorySchema
from ..shared.Authentication import Auth
UPLOAD_DIRECTORY = '/uploads/listings'
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg'])
listing_api = Blueprint('listings', __name__)
listing_schema = ListingSchema()
category_schema = CategorySchema()
...
@listing_api.route('/', methods=['POST'])
@Auth.auth_required
def create():
"""
Create Listing Function
"""
req_data = request.get_json()
req_files = request.files
data, error = listing_schema.load(req_data, req_files)
if error:
return custom_response(error, 400)
# # Upload file
# # check if the post request has the file part
# if 'uploaded_files' not in request.files:
# print('No file part')
# #return redirect(request.url)
# files = request.files.getlist('uploaded_files')
# # if user does not select file, browser also
# if files and allowed_file(files.filename):
# for file in files:
# filename = secure_filename(file.filename)
# file.save(os.path.join(UPLOAD_DIRECTORY, filename))
print('-----eee')
print (req_files['uploaded_files'])
if req_files:
if 'uploaded_files' in req_files:
filename = images.save(req_files['uploaded_files'])
self.uploaded_files = secure_filename(filename)
#self.image_url = images.url(filename)
file.save(os.path.join(UPLOAD_DIRECTORY, filename))
listing = ListingModel(data)
listing.save()
ser_data = listing_schema.dump(listing).data
return custom_response('Created', 201)
...
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def custom_response(res, status_code):
"""
Custom Response Function
"""
return Response(
mimetype="application/json",
response=json.dumps(res),
status=status_code
)
这是我的nuxt.js视图:
<template>
<div class="listing">
<div v-if="user.email_confirmed == 1" class="container">
<h2 class="title">Add Item for Rent</h2>
<form enctype="multipart/form-data" @submit.prevent="createListing">
<div class="columns">
<div class="column is-three-quarters">
<b-field
label="Title"
:type="{ 'is-danger': errors.has('title') }"
:message="errors.first('title')"
>
<b-input
v-model="title"
v-validate="'required|alpha_num'"
name="title"
maxlength="20"
placeholder="Title (e.g Cordless Drill)"
></b-input>
</b-field>
<b-field
label="Description"
type="{ 'is-danger': errors.has('description') }"
:message="errors.first('description')"
>
<b-input
v-model="description"
v-validate="'required|alpha_spaces'"
name="description"
maxlength="500"
type="textarea"
placeholder="Description (e.g Ryobi Cordless Drill + spare battery.)"
></b-input>
</b-field>
<span>{{ errors.first('category') }}</span>
<span>{{ errors.first('location') }}</span>
<span>{{ errors.first('available_from') }}</span>
<span>{{ errors.first('available_until') }}</span>
<span>{{ errors.first('price') }}</span>
<b-field
label=""
type="{ 'is-danger': errors.has('category'), 'is-danger': errors.has('location'), 'is-danger': errors.has('available_from'), 'is-danger': errors.has('available_until'), 'is-danger': errors.has('price') }"
>
<b-select
v-model="selected_category"
v-validate="'required'"
name="category"
placeholder="Category"
@change.native="getSelectedCategoryName"
>
<option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</option>
</b-select>
<b-input
v-model="location"
v-validate="'required'"
name="location"
placeholder="Location (e.g Durban)"
></b-input>
<b-datepicker
v-model="available_from"
v-validate="
'required|date_format:dd/MM/yyyy|before:available_until'
"
placeholder="Available from..."
icon="calendar-today"
>
</b-datepicker>
<b-datepicker
ref="available_until"
v-model="available_until"
v-validate="'required'"
placeholder="Availabe to..."
icon="calendar-today"
>
</b-datepicker>
<!-- R Rand symbol -->
<b-input
type="number"
v-model="price"
v-validate="'required|numeric'"
maxlength="5"
placeholder="Price (e.g R500)"
></b-input>
</b-field>
<b-field
label=""
type="{ 'is-danger': errors.has('category'), 'is-danger': errors.has('location'), 'is-danger': errors.has('available_from'), 'is-danger': errors.has('available_until'), 'is-danger': errors.has('price') }"
>
<b-input
v-model="item_quantity"
v-validate="'required|numeric'"
maxlength="5"
placeholder="Number of units"
></b-input>
<b-input
v-model="replacement_value"
v-validate="'required|numeric'"
maxlength="5"
placeholder="Replacement value"
></b-input>
</b-field>
<b-field>
<b-upload
v-model="dropFiles"
v-validate="'image'"
name="dropFiles"
multiple
drag-drop
>
<section class="section">
<div class="content has-text-centered">
<p>
<b-icon icon="upload" size="is-large"></b-icon>
</p>
<p>Drop your files here or click to upload</p>
</div>
</section>
</b-upload>
</b-field>
<div class="tags">
<span
v-for="(file, index) in dropFiles"
:key="index"
class="tag is-primary"
>
{{ file.name }}
<button
class="delete is-small"
type="button"
@click="deleteDropFile(index)"
></button>
</span>
</div>
</div>
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<ul class="carousel carousel--thumb">
<input
id="K"
class="carousel__activator"
type="radio"
name="thumb"
checked="checked"
/>
<input
id="L"
class="carousel__activator"
type="radio"
name="thumb"
/>
<input
id="M"
class="carousel__activator"
type="radio"
name="thumb"
/>
<input
id="N"
class="carousel__activator"
type="radio"
name="thumb"
/>
<input
id="O"
class="carousel__activator"
type="radio"
name="thumb"
/>
<div class="carousel__controls">
<label
class="carousel__control carousel__control--backward"
for="O"
></label>
<label
class="carousel__control carousel__control--forward"
for="L"
></label>
</div>
<div class="carousel__controls">
<label
class="carousel__control carousel__control--backward"
for="K"
></label>
<label
class="carousel__control carousel__control--forward"
for="M"
></label>
</div>
<div class="carousel__controls">
<label
class="carousel__control carousel__control--backward"
for="L"
></label>
<label
class="carousel__control carousel__control--forward"
for="N"
></label>
</div>
<div class="carousel__controls">
<label
class="carousel__control carousel__control--backward"
for="M"
></label>
<label
class="carousel__control carousel__control--forward"
for="O"
></label>
</div>
<div class="carousel__controls">
<label
class="carousel__control carousel__control--backward"
for="N"
></label>
<label
class="carousel__control carousel__control--forward"
for="K"
></label>
</div>
<li class="carousel__slide"></li>
<li class="carousel__slide"></li>
<li class="carousel__slide"></li>
<li class="carousel__slide"></li>
<li class="carousel__slide"></li>
<div class="carousel__indicators">
<label class="carousel__indicator" for="K"></label>
<label class="carousel__indicator" for="L"></label>
<label class="carousel__indicator" for="M"></label>
<label class="carousel__indicator" for="N"></label>
<label class="carousel__indicator" for="O"></label>
</div>
</ul>
<div v-if="price != null" class="tags has-addons is-overlay">
<span class="tag is-dark is-radiusless is-bottom">
R{{ price | formatNumber }}
</span>
<span class="tag is-info is-radiusless is-bottom">
Day
</span>
</div>
</div>
<div class="card-content">
<div class="content" style="word-wrap: break-word;">
<h3 class="title is-capitalized">{{ title }}</h3>
{{ selected_category_name }}<br />
<span v-if="item_quantity != null">
{{ item_quantity }} units available<br />
</span>
<span v-if="description != null">
{{ description | truncate(150) }}
</span>
<br />
<a>{{ location }}</a>
<br />
{{ available_from | moment('DD MMM YYYY') }}
<span v-if="available_until != null">-</span>
{{ available_until | moment('DD MMM YYYY') }}<br />
<span v-if="user !== 'undefined'">
<!-- Used full surname because there's an error when displaying only 1st char -->
{{ user.firstname }} {{ user.surname }}.
</span>
</div>
</div>
</div>
<br />
<button
id="create-listing-button"
class="button is-primary is-medium is-fullwidth"
>
List Item
</button>
</div>
</div>
</form>
</div>
<div v-if="!user && user.email_confirmed != 1" class="container">
<p>
Please confirm your email address to access this page.
<a href="#">Resend activation email.</a>
</p>
</div>
</div>
</template>
<script>
import moment from 'moment'
import axios from 'axios'
export default {
name: 'CreateListingPage',
data() {
return {
title: null,
description: null,
selected_category: null,
selected_category_name: null,
categories: [],
price: null,
location: null,
item_quantity: null,
replacement_value: null,
available_from: null,
available_until: null,
dropFiles: [],
user: []
}
},
mounted() {
// If JWT does not exist, redirect to login page.
if (this.$cookies.isKey('client_token') === false) {
this.$router.push('/auth/login')
} else {
this.getUser()
this.getCategories()
}
},
methods: {
getUser() {
// Set header jwt-token
const config = {
headers: {
'Content-Type': 'application/json',
'api-token': self.$cookies.get('client_token')
}
}
// Request user's info and update local user array
axios
.get(process.env.API_URL + '/resources/users/me', config)
.then(response => (this.user = response.data))
.catch(function(error) {
alert(error)
})
},
getCategories() {
// Set header jwt-token
const config = {
headers: {
'Content-Type': 'application/json',
'api-token': self.$cookies.get('client_token')
}
}
// Request categories info and update local categories array
axios
.get(process.env.API_URL + '/resources/categories/', config)
// Reverse array to show latest record first
.then(response => (this.categories = response.data))
.catch(function(error) {
alert(error)
})
},
deleteDropFile(index) {
this.dropFiles.splice(index, 1)
},
createListing() {
this.$validator.validateAll().then(result => {
if (result) {
// If form is valid, add listing
const self = this
// Set header jwt-token
const config = {
headers: {
'Content-Type': 'application/json',
'api-token': self.$cookies.get('client_token')
}
}
// alert(moment(this.available_from).format('YYYY-MM-DD HH:mm:ss'))
axios
.post(
process.env.API_URL + '/resources/listings/',
{
category_id: this.selected_category,
owner_id: this.user.id,
title: this.title,
location: this.location,
description: this.description,
price: this.price,
available_from: moment(this.available_from).format(
'YYYY-MM-DD HH:mm:ss'
),
available_until: moment(this.available_until).format(
'YYYY-MM-DD HH:mm:ss'
),
available_items: this.item_quantity,
item_quantity: this.item_quantity,
uploaded_files: this.dropFiles,
replacement_value: this.replacement_value
},
config
)
.then(response => {
this.$router.push('/users/profile')
})
.catch(function(error) {
// Show server response if the request doesn't return a HTTP 201 status code.
self.$toast.open({
// message: error,
message: error.response.data.error,
type: 'is-danger',
position: 'is-bottom'
})
})
} else {
// Show message if form field(s) are invalid.
this.$toast.open({
message: 'Form is not valid! Please check the fields.',
type: 'is-danger',
position: 'is-bottom'
})
}
})
},
getSelectedCategoryName(e) {
if (e.target.options.selectedIndex > -1) {
this.selected_category_name =
e.target.options[e.target.options.selectedIndex].text
}
}
}
}
</script>
<style>
.is-bottom {
margin-top: auto;
}
.currencyinput {
border: 1px inset #ccc;
}
.currencyinput input {
border: 0;
}
.fa-zar:before {
font-weight: bold;
content: 'R';
}
</style>
知道我在做什么错吗?我的猜测是正在提交数据而不是文件?
我已将axios帖子更改为:
createListing() {
this.$validator.validateAll().then(result => {
if (result) {
// If form is valid, add listing
const self = this
// Set header jwt-token
const config = {
headers: {
// 'Content-Type': 'application/json',
'Content-Type': 'multipart/form-data',
'api-token': self.$cookies.get('client_token')
}
}
// alert(moment(this.available_from).format('YYYY-MM-DD HH:mm:ss'))
// axios
// .post(
// process.env.API_URL + '/resources/listings/',
// {
// category_id: this.selected_category,
// owner_id: this.user.id,
// title: this.title,
// location: this.location,
// description: this.description,
// price: this.price,
// available_from: moment(this.available_from).format(
// 'YYYY-MM-DD HH:mm:ss'
// ),
// available_until: moment(this.available_until).format(
// 'YYYY-MM-DD HH:mm:ss'
// ),
// available_items: this.item_quantity,
// item_quantity: this.item_quantity,
// uploaded_files: this.dropFiles,
// replacement_value: this.replacement_value
// },
// config
// )
const formData = new FormData()
const imagefile = document.querySelector('#dropFiles')
const categoryid = document.querySelector('#category_id')
formData.append('uploaded_files', imagefile.files[0])
formData.append('category_id', categoryid)
axios
.post(
process.env.API_URL + '/resources/listings/',
{
formData
},
config
)
.then(response => {
this.$router.push('/users/profile')
})
.catch(function(error) {
// Show server response if the request doesn't return a HTTP 201 status code.
self.$toast.open({
// message: error,
message: error.response.data.error,
type: 'is-danger',
position: 'is-bottom'
})
})
} else {
// Show message if form field(s) are invalid.
this.$toast.open({
message: 'Form is not valid! Please check the fields.',
type: 'is-danger',
position: 'is-bottom'
})
}
})
}
这是我的终结点:
@listing_api.route('/', methods=['POST'])
@Auth.auth_required
def create():
"""
Create Listing Function
"""
req_data = request.form.get('listing')
req_files = request.files
data, error = listing_schema.load(req_data, req_files)
if error:
return custom_response(error, 400)
# # Upload file
# # check if the post request has the file part
# if 'uploaded_files' not in request.files:
# print('No file part')
# #return redirect(request.url)
# files = request.files.getlist('uploaded_files')
# # if user does not select file, browser also
# if files and allowed_file(files.filename):
# for file in files:
# filename = secure_filename(file.filename)
# file.save(os.path.join(UPLOAD_DIRECTORY, filename))
if req_files:
print('-----eee')
print (req_files['uploaded_files'])
if 'uploaded_files' in req_files:
filename = images.save(req_files['uploaded_files'])
self.uploaded_files = secure_filename(filename)
#self.image_url = images.url(filename)
file.save(os.path.join(UPLOAD_DIRECTORY, filename))
listing = ListingModel(data)
listing.save()
ser_data = listing_schema.dump(listing).data
return custom_response('Created', 201)
但是当我运行代码时,我得到了:
self.category_id = data.get('category_id')
AttributeError: 'NoneType' object has no attribute 'get'
看起来端点现在没有获取表单数据吗?
如何将整个表单作为一个对象?
打印request.get_json()
时得到None
。