Brak opisu

__init__.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. # -*- coding: utf-8 -*-
  2. """
  3. flask_dropzone
  4. ~~~~~~~~~~~~~~
  5. :author: Grey Li <withlihui@gmail.com>
  6. :copyright: (c) 2017 by Grey Li.
  7. :license: MIT, see LICENSE for more details.
  8. """
  9. import warnings
  10. from flask import Blueprint
  11. from markupsafe import Markup
  12. from flask import current_app
  13. from flask import render_template_string
  14. from flask import url_for
  15. from app.flask_dropzone.utils import get_url
  16. #: defined normal file type
  17. allowed_file_extensions = {
  18. 'default': 'image/*, audio/*, video/*, text/*, application/*',
  19. 'image': 'image/*',
  20. 'audio': 'audio/*',
  21. 'video': 'video/*',
  22. 'text': 'text/*',
  23. 'app': 'application/*'
  24. }
  25. class _Dropzone(object):
  26. @staticmethod
  27. def load(js_url='', css_url='', version='5.2.0'):
  28. """Load Dropzone resources with given version and init dropzone configuration.
  29. .. versionchanged:: 1.4.3
  30. Added ``js_url`` and ``css_url`` parameters to pass custom resource URL.
  31. .. versionchanged:: 1.4.4
  32. This method was deprecated due to inflexible. Now it's divided into three methods:
  33. 1. Use ``load_css()`` to load css resources.
  34. 2. Use ``load_js()`` to load js resources.
  35. 3. Use ``config()`` to configure Dropzone.
  36. :param js_url: The JavaScript url for Dropzone.js.
  37. :param css_url: The CSS url for Dropzone.js.
  38. :param version: The version of Dropzone.js.
  39. """
  40. warnings.warn('The method will be removed in 2.0, see docs for more details.')
  41. js_filename = 'dropzone.min.js'
  42. css_filename = 'dropzone.min.css'
  43. upload_multiple = current_app.config['DROPZONE_UPLOAD_MULTIPLE']
  44. parallel_uploads = current_app.config['DROPZONE_PARALLEL_UPLOADS']
  45. if upload_multiple in [True, 'true', 'True', 1]:
  46. upload_multiple = 'true'
  47. else:
  48. upload_multiple = 'false'
  49. serve_local = current_app.config['DROPZONE_SERVE_LOCAL']
  50. size = current_app.config['DROPZONE_MAX_FILE_SIZE']
  51. param = current_app.config['DROPZONE_INPUT_NAME']
  52. redirect_view = current_app.config['DROPZONE_REDIRECT_VIEW']
  53. if redirect_view is not None:
  54. redirect_js = '''
  55. this.on("queuecomplete", function(file) {
  56. // Called when all files in the queue finish uploading.
  57. window.location = "%s";
  58. });''' % url_for(redirect_view)
  59. else:
  60. redirect_js = ''
  61. if not current_app.config['DROPZONE_ALLOWED_FILE_CUSTOM']:
  62. allowed_type = allowed_file_extensions[
  63. current_app.config['DROPZONE_ALLOWED_FILE_TYPE']]
  64. else:
  65. allowed_type = current_app.config['DROPZONE_ALLOWED_FILE_TYPE']
  66. max_files = current_app.config['DROPZONE_MAX_FILES']
  67. default_message = current_app.config['DROPZONE_DEFAULT_MESSAGE']
  68. invalid_file_type = current_app.config['DROPZONE_INVALID_FILE_TYPE']
  69. file_too_big = current_app.config['DROPZONE_FILE_TOO_BIG']
  70. server_error = current_app.config['DROPZONE_SERVER_ERROR']
  71. browser_unsupported = current_app.config['DROPZONE_BROWSER_UNSUPPORTED']
  72. max_files_exceeded = current_app.config['DROPZONE_MAX_FILE_EXCEED']
  73. cancelUpload = current_app.config['DROPZONE_CANCEL_UPLOAD']
  74. removeFile = current_app.config['DROPZONE_REMOVE_FILE']
  75. cancelConfirmation = current_app.config['DROPZONE_CANCEL_CONFIRMATION']
  76. uploadCanceled = current_app.config['DROPZONE_UPLOAD_CANCELED']
  77. timeout = current_app.config['DROPZONE_TIMEOUT']
  78. if timeout:
  79. timeout_js = 'timeout: %d,' % timeout
  80. else:
  81. timeout_js = ''
  82. if serve_local:
  83. js = '<script src="%s"></script>\n' % url_for('dropzone.static', filename=js_filename)
  84. css = '<link rel="stylesheet" href="%s" type="text/css">\n' % \
  85. url_for('dropzone.static', filename=css_filename)
  86. else:
  87. js = '<script src="https://cdn.jsdelivr.net/npm/dropzone@%s/dist/%s"></script>\n' % (version, js_filename)
  88. css = '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dropzone@%s/dist/min/%s"' \
  89. ' type="text/css">\n' % (version, css_filename)
  90. if js_url:
  91. js = '<script src="%s"></script>\n' % js_url
  92. if css_url:
  93. css = '<link rel="stylesheet" href="%s" type="text/css">\n' % css_url
  94. return Markup('''
  95. %s%s<script>
  96. Dropzone.options.myDropzone = {
  97. init: function() {%s},
  98. uploadMultiple: %s,
  99. parallelUploads: %d,
  100. paramName: "%s", // The name that will be used to transfer the file
  101. maxFilesize: %d, // MB
  102. acceptedFiles: "%s",
  103. maxFiles: %s,
  104. dictDefaultMessage: "%s", // message display on drop area
  105. dictFallbackMessage: "%s",
  106. dictInvalidFileType: "%s",
  107. dictFileTooBig: "%s",
  108. dictResponseError: "%s",
  109. dictMaxFilesExceeded: "%s",
  110. dictCancelUpload: "%s",
  111. dictRemoveFile: "%s",
  112. dictCancelUploadConfirmation: "%s",
  113. dictUploadCanceled: "%s",
  114. %s // timeout
  115. };
  116. </script>
  117. ''' % (css, js, redirect_js, upload_multiple, parallel_uploads, param, size, allowed_type, max_files,
  118. default_message, browser_unsupported, invalid_file_type, file_too_big, server_error,
  119. max_files_exceeded, cancelUpload, removeFile, cancelConfirmation, uploadCanceled, timeout_js))
  120. @staticmethod
  121. def load_css(css_url=None, version='5.2.0'):
  122. """Load Dropzone's css resources with given version.
  123. .. versionadded:: 1.4.4
  124. :param css_url: The CSS url for Dropzone.js.
  125. :param version: The version of Dropzone.js.
  126. """
  127. css_filename = 'dropzone.min.css'
  128. serve_local = current_app.config['DROPZONE_SERVE_LOCAL']
  129. if serve_local:
  130. css = '<link rel="stylesheet" href="%s" type="text/css">\n' % \
  131. url_for('dropzone.static', filename=css_filename)
  132. else:
  133. css = '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dropzone@%s/dist/min/%s"' \
  134. ' type="text/css">\n' % (version, css_filename)
  135. if css_url:
  136. css = '<link rel="stylesheet" href="%s" type="text/css">\n' % css_url
  137. return Markup(css)
  138. @staticmethod
  139. def load_js(js_url=None, version='5.2.0'):
  140. """Load Dropzone's js resources with given version.
  141. .. versionadded:: 1.4.4
  142. :param js_url: The JS url for Dropzone.js.
  143. :param version: The version of Dropzone.js.
  144. """
  145. js_filename = 'dropzone.min.js'
  146. serve_local = current_app.config['DROPZONE_SERVE_LOCAL']
  147. if serve_local:
  148. js = '<script src="%s"></script>\n' % url_for('dropzone.static', filename=js_filename)
  149. else:
  150. js = '<script src="https://cdn.jsdelivr.net/npm/dropzone@%s/dist/%s"></script>\n' % (version, js_filename)
  151. if js_url:
  152. js = '<script src="%s"></script>\n' % js_url
  153. return Markup(js)
  154. @staticmethod
  155. def config(redirect_url=None, custom_init='', custom_options='', **kwargs):
  156. """Initialize dropzone configuration.
  157. .. versionadded:: 1.4.4
  158. :param redirect_url: The URL to redirect when upload complete.
  159. :param custom_init: Custom javascript code in ``init: function() {}``.
  160. :param custom_options: Custom javascript code in ``Dropzone.options.myDropzone = {}``.
  161. :param **kwargs: Mirror configuration variable, lowercase and without prefix.
  162. For example, ``DROPZONE_UPLOAD_MULTIPLE`` becomes ``upload_multiple`` here.
  163. """
  164. if custom_init and not custom_init.strip().endswith(';'):
  165. custom_init += ';'
  166. if custom_options and not custom_options.strip().endswith(','):
  167. custom_options += ','
  168. upload_multiple = kwargs.get('upload_multiple', current_app.config['DROPZONE_UPLOAD_MULTIPLE'])
  169. parallel_uploads = kwargs.get('parallel_uploads', current_app.config['DROPZONE_PARALLEL_UPLOADS'])
  170. if upload_multiple in [True, 'true', 'True', 1]:
  171. upload_multiple = 'true'
  172. else:
  173. upload_multiple = 'false'
  174. size = kwargs.get('max_file_size', current_app.config['DROPZONE_MAX_FILE_SIZE'])
  175. param = kwargs.get('input_name', current_app.config['DROPZONE_INPUT_NAME'])
  176. redirect_view = kwargs.get('redirect_view', current_app.config['DROPZONE_REDIRECT_VIEW'])
  177. if redirect_view is not None or redirect_url is not None:
  178. redirect_url = redirect_url or url_for(redirect_view)
  179. redirect_js = '''
  180. this.on("queuecomplete", function(file) {
  181. // Called when all files in the queue finish uploading.
  182. window.location = "%s";
  183. });''' % redirect_url
  184. else:
  185. redirect_js = ''
  186. max_files = kwargs.get('max_files', current_app.config['DROPZONE_MAX_FILES'])
  187. click_upload = kwargs.get('upload_on_click', current_app.config['DROPZONE_UPLOAD_ON_CLICK'])
  188. button_id = kwargs.get('upload_btn_id', current_app.config['DROPZONE_UPLOAD_BTN_ID'])
  189. in_form = kwargs.get('in_form', current_app.config['DROPZONE_IN_FORM'])
  190. cancelUpload = kwargs.get('cancel_upload', current_app.config['DROPZONE_CANCEL_UPLOAD'])
  191. removeFile = kwargs.get('remove_file', current_app.config['DROPZONE_REMOVE_FILE'])
  192. cancelConfirmation = kwargs.get('cancel_confirmation', current_app.config['DROPZONE_CANCEL_CONFIRMATION'])
  193. uploadCanceled = kwargs.get('upload_canceled', current_app.config['DROPZONE_UPLOAD_CANCELED'])
  194. if click_upload:
  195. if in_form:
  196. action = get_url(kwargs.get('upload_action', current_app.config['DROPZONE_UPLOAD_ACTION']))
  197. click_listener = '''
  198. dz = this; // Makes sure that 'this' is understood inside the functions below.
  199. document.getElementById("%s").addEventListener("click", function handler(e) {
  200. e.currentTarget.removeEventListener(e.type, handler);
  201. e.preventDefault();
  202. e.stopPropagation();
  203. dz.processQueue();
  204. });
  205. this.on("queuecomplete", function(file) {
  206. // Called when all files in the queue finish uploading.
  207. document.getElementById("%s").click();
  208. });
  209. ''' % (button_id, button_id)
  210. click_option = '''
  211. url: "%s",
  212. autoProcessQueue: false,
  213. // addRemoveLinks: true,
  214. ''' % action
  215. else:
  216. click_listener = '''
  217. dz = this;
  218. document.getElementById("%s").addEventListener("click", function handler(e) {dz.processQueue();});
  219. ''' % button_id
  220. click_option = '''
  221. autoProcessQueue: false,
  222. // addRemoveLinks: true,
  223. '''
  224. upload_multiple = 'true'
  225. parallel_uploads = max_files if isinstance(max_files, int) else parallel_uploads
  226. else:
  227. click_listener = ''
  228. click_option = ''
  229. allowed_file_type = kwargs.get('allowed_file_type', current_app.config['DROPZONE_ALLOWED_FILE_TYPE'])
  230. allowed_file_custom = kwargs.get('allowed_file_custom', current_app.config['DROPZONE_ALLOWED_FILE_CUSTOM'])
  231. if allowed_file_custom:
  232. allowed_type = allowed_file_type
  233. else:
  234. allowed_type = allowed_file_extensions[allowed_file_type]
  235. default_message = kwargs.get('default_message', current_app.config['DROPZONE_DEFAULT_MESSAGE'])
  236. invalid_file_type = kwargs.get('invalid_file_type', current_app.config['DROPZONE_INVALID_FILE_TYPE'])
  237. file_too_big = kwargs.get('file_too_big', current_app.config['DROPZONE_FILE_TOO_BIG'])
  238. server_error = kwargs.get('server_error', current_app.config['DROPZONE_SERVER_ERROR'])
  239. browser_unsupported = kwargs.get('browser_unsupported', current_app.config['DROPZONE_BROWSER_UNSUPPORTED'])
  240. max_files_exceeded = kwargs.get('max_file_exceeded', current_app.config['DROPZONE_MAX_FILE_EXCEED'])
  241. timeout = kwargs.get('timeout', current_app.config['DROPZONE_TIMEOUT'])
  242. if timeout:
  243. custom_options += 'timeout: %d,' % timeout
  244. enable_csrf = kwargs.get('enable_csrf', current_app.config['DROPZONE_ENABLE_CSRF'])
  245. if enable_csrf:
  246. if 'csrf' not in current_app.extensions:
  247. raise RuntimeError("CSRFProtect is not initialized. It's required to enable CSRF protect, \
  248. see docs for more details.")
  249. csrf_token = render_template_string('{{ csrf_token() }}')
  250. custom_options += 'headers: {"X-CSRF-Token": "%s"},' % csrf_token
  251. return Markup('''<script>
  252. Dropzone.options.myDropzone = {
  253. init: function() {
  254. %s // redirect after queue complete
  255. %s // upload queue when button click
  256. %s // custom init code
  257. },
  258. %s // click upload options
  259. uploadMultiple: %s,
  260. parallelUploads: %d,
  261. paramName: "%s", // The name that will be used to transfer the file
  262. maxFilesize: %d, // MB
  263. acceptedFiles: "%s",
  264. maxFiles: %s,
  265. dictDefaultMessage: `%s`, // message display on drop area
  266. dictFallbackMessage: "%s",
  267. dictInvalidFileType: "%s",
  268. dictFileTooBig: "%s",
  269. dictResponseError: "%s",
  270. dictMaxFilesExceeded: "%s",
  271. dictCancelUpload: "%s",
  272. dictRemoveFile: "%s",
  273. dictCancelUploadConfirmation: "%s",
  274. dictUploadCanceled: "%s",
  275. %s // custom options code
  276. };
  277. </script>
  278. ''' % (redirect_js, click_listener, custom_init, click_option,
  279. upload_multiple, parallel_uploads, param, size, allowed_type, max_files,
  280. default_message, browser_unsupported, invalid_file_type, file_too_big,
  281. server_error, max_files_exceeded, cancelUpload, removeFile, cancelConfirmation,
  282. uploadCanceled, custom_options))
  283. @staticmethod
  284. def create(action='', csrf=False, action_view='', **kwargs):
  285. """Create a Dropzone form with given action.
  286. .. versionchanged:: 1.4.2
  287. Added ``csrf`` parameter to enable CSRF protect.
  288. .. versionchanged:: 1.4.3
  289. Added ``action`` parameter to replace ``action_view``, ``action_view`` was deprecated now.
  290. .. versionchanged:: 1.5.0
  291. If ``DROPZONE_IN_FORM`` set to ``True``, create ``<div>`` instead of ``<form>``.
  292. .. versionchanged:: 1.5.4
  293. ``csrf`` was deprecated now.
  294. :param action: The action attribute in ``<form>``, pass the url which handle uploads.
  295. :param csrf: Enable CSRF protect or not, same with ``DROPZONE_ENABLE_CSRF``, deprecated since 1.5.4.
  296. :param action_view: The view which handle the post data, deprecated since 1.4.2.
  297. """
  298. if current_app.config['DROPZONE_IN_FORM']:
  299. return Markup('<div class="dropzone" id="myDropzone"></div>')
  300. if action:
  301. action_url = get_url(action)
  302. else:
  303. warnings.warn('The argument was renamed to "action" and will be removed in 2.0.')
  304. action_url = url_for(action_view, **kwargs)
  305. if csrf:
  306. warnings.warn('The argument was deprecated and will be removed in 2.0, use DROPZONE_ENABLE_CSRF instead.')
  307. return Markup('''<form action="%s" method="post" class="dropzone" id="myDropzone"
  308. enctype="multipart/form-data"></form>''' % action_url)
  309. @staticmethod
  310. def style(css):
  311. """Add css to dropzone.
  312. :param css: style sheet code.
  313. """
  314. return Markup('<style>\n.dropzone{%s}\n</style>' % css)
  315. class Dropzone(object):
  316. def __init__(self, app=None):
  317. if app is not None:
  318. self.init_app(app)
  319. def init_app(self, app):
  320. blueprint = Blueprint('dropzone', __name__)
  321. app.register_blueprint(blueprint)
  322. if not hasattr(app, 'extensions'):
  323. app.extensions = {}
  324. app.extensions['dropzone'] = _Dropzone
  325. app.context_processor(self.context_processor)
  326. # settings
  327. app.config.setdefault('DROPZONE_SERVE_LOCAL', False)
  328. app.config.setdefault('DROPZONE_MAX_FILE_SIZE', 3) # MB
  329. app.config.setdefault('DROPZONE_INPUT_NAME', 'file')
  330. app.config.setdefault('DROPZONE_ALLOWED_FILE_CUSTOM', False)
  331. app.config.setdefault('DROPZONE_ALLOWED_FILE_TYPE', 'default')
  332. app.config.setdefault('DROPZONE_MAX_FILES', 'null')
  333. # The timeout to cancel upload request in millisecond, default to 30000 (30 second).
  334. # Set a large number if you need to upload large file.
  335. # .. versionadded: 1.5.0
  336. app.config.setdefault('DROPZONE_TIMEOUT', None) # millisecond, default to 30000 (30 second)
  337. # The view to redirect when upload was completed.
  338. # .. versionadded:: 1.4.1
  339. app.config.setdefault('DROPZONE_REDIRECT_VIEW', None)
  340. # Whether to send multiple files in one request.
  341. # In default, each file will send with a request.
  342. # Then you can use ``request.files.getlist('paramName')`` to
  343. # get a list of uploads.
  344. # .. versionadded:: 1.4.1
  345. app.config.setdefault('DROPZONE_UPLOAD_MULTIPLE', False)
  346. # When ``DROPZONE_UPLOAD_MULTIPLE`` set to True, this will
  347. # defined how many uploads will handled in per request.
  348. # .. versionadded:: 1.4.1
  349. app.config.setdefault('DROPZONE_PARALLEL_UPLOADS', 2)
  350. # When set to ``True``, it will add a csrf_token hidden field in upload form.
  351. # You have to install Flask-WTF to make it work properly, see details in docs.
  352. # .. versionadded:: 1.4.2
  353. app.config.setdefault('DROPZONE_ENABLE_CSRF', False)
  354. # Add support to upload files when button was clicked.
  355. # .. versionadded:: 1.5.0
  356. app.config.setdefault('DROPZONE_UPLOAD_ACTION', '')
  357. app.config.setdefault('DROPZONE_UPLOAD_ON_CLICK', False)
  358. app.config.setdefault('DROPZONE_UPLOAD_BTN_ID', 'upload')
  359. # Add support to create dropzone inside ``<form>``.
  360. # .. versionadded:: 1.5.0
  361. app.config.setdefault('DROPZONE_IN_FORM', False)
  362. # messages
  363. app.config.setdefault('DROPZONE_DEFAULT_MESSAGE', "Drop files here or click to upload.")
  364. app.config.setdefault('DROPZONE_INVALID_FILE_TYPE', "You can't upload files of this type.")
  365. app.config.setdefault('DROPZONE_FILE_TOO_BIG',
  366. "File is too big {{filesize}}. Max filesize: {{maxFilesize}}MiB.")
  367. app.config.setdefault('DROPZONE_SERVER_ERROR', "Server error: {{statusCode}}")
  368. app.config.setdefault('DROPZONE_BROWSER_UNSUPPORTED',
  369. "Your browser does not support drag'n'drop file uploads.")
  370. app.config.setdefault('DROPZONE_MAX_FILE_EXCEED', "You can't upload any more files.")
  371. app.config.setdefault('DROPZONE_CANCEL_UPLOAD', "Cancel upload")
  372. app.config.setdefault('DROPZONE_REMOVE_FILE', "Remove file")
  373. app.config.setdefault('DROPZONE_CANCEL_CONFIRMATION', "You really want to delete this file?")
  374. app.config.setdefault('DROPZONE_UPLOAD_CANCELED', "Upload canceled")
  375. @staticmethod
  376. def context_processor():
  377. return {
  378. 'dropzone': current_app.extensions['dropzone']
  379. }