Flask-同时上传文件和数据

时间:2019-07-29 07:54:46

标签: python flask axios nuxt.js

我有一个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>
              &nbsp;
              <b-input
                v-model="location"
                v-validate="'required'"
                name="location"
                placeholder="Location (e.g Durban)"
              ></b-input>
              &nbsp;
              <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>
              &nbsp;
              <b-datepicker
                ref="available_until"
                v-model="available_until"
                v-validate="'required'"
                placeholder="Availabe to..."
                icon="calendar-today"
              >
              </b-datepicker>
              &nbsp;
              <!-- &#82; 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>
              &nbsp;
              <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

0 个答案:

没有答案