├── .gitignore ├── .travis.yml ├── .tx └── config ├── AUTHORS ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── Screenshot.png ├── admin.rst ├── api.rst ├── changelog.rst ├── conf.py ├── faq.rst ├── fieldswidgets.rst ├── filelisting.rst ├── fileobject.rst ├── help.rst ├── index.rst ├── quickstart.rst ├── releasenotes.rst ├── settings.rst ├── testing.rst ├── translation.rst ├── troubleshooting.rst └── versions.rst ├── filebrowser ├── __init__.py ├── actions.py ├── admin.py ├── base.py ├── compat.py ├── decorators.py ├── fields.py ├── forms.py ├── locale │ ├── az │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── bg_BG │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── is │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ja_JP │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── mn │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── pl_PL │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru_RU │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sl_SI │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── sv │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── tr_TR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── vi │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── fb_version_generate.py │ │ └── fb_version_remove.py ├── models.py ├── namers.py ├── settings.py ├── signals.py ├── sites.py ├── static │ └── filebrowser │ │ ├── css │ │ ├── filebrowser.css │ │ └── uploadfield.css │ │ ├── img │ │ ├── TEST_IMAGE_000.jpg │ │ ├── cancel.png │ │ ├── cancel_hover.png │ │ ├── completed.png │ │ ├── fb-upload-spinner.gif │ │ ├── fb-upload.png │ │ ├── fb-upload_hover.png │ │ ├── filebrowser_icon_show.gif │ │ ├── filebrowser_icon_show_hover.gif │ │ ├── icon-pulldown-versions-active.png │ │ ├── icon-pulldown-versions-hover.png │ │ ├── icon-pulldown-versions.png │ │ ├── progress-bar-content.png │ │ ├── test_not_an_image.jpg │ │ └── testimage.jpg │ │ └── js │ │ ├── AddFileBrowser.js │ │ ├── FB_CKEditor.js │ │ ├── FB_FileBrowseField.js │ │ ├── FB_TinyMCE.js │ │ ├── FB_TinyMCEv4.js │ │ ├── FB_TinyMCEv5.js │ │ ├── TinyMCEAdmin.js │ │ ├── TinyMCEv4Admin.js │ │ ├── TinyMCEv5Admin.js │ │ └── fileuploader.js ├── storage.py ├── templates │ └── filebrowser │ │ ├── createdir.html │ │ ├── custom_field.html │ │ ├── custom_upload_field.html │ │ ├── delete_confirm.html │ │ ├── detail.html │ │ ├── include │ │ ├── _response.html │ │ ├── breadcrumbs.html │ │ ├── filelisting.html │ │ ├── filter.html │ │ ├── paginator.html │ │ ├── tableheader.html │ │ └── toolbar.html │ │ ├── index.html │ │ ├── upload.html │ │ └── version.html ├── templatetags │ ├── __init__.py │ ├── fb_compat.py │ ├── fb_csrf.py │ ├── fb_pagination.py │ ├── fb_tags.py │ └── fb_versions.py └── utils.py ├── manual_test.sh ├── runtests.py ├── runtests_in_env.sh ├── setup.py ├── tests ├── __init__.py ├── base.py ├── models.py ├── requirements.txt ├── settings.py ├── test_base.py ├── test_commands.py ├── test_decorators.py ├── test_fields.py ├── test_namers.py ├── test_settings.py ├── test_sites.py ├── test_templatetags.py ├── test_versions.py └── urls.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | docs/_build/ 3 | dist 4 | *.egg-info 5 | *.pot 6 | .DS_store 7 | fabfile.py 8 | NOTES.md 9 | .tox/* 10 | 11 | /envs 12 | /projects -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install tox 4 | - pip install coverage 5 | script: 6 | - tox 7 | matrix: 8 | include: 9 | - python: "2.7" 10 | env: TOXENV=py27-django111 11 | - python: "3.4" 12 | env: TOXENV=py34-django111 13 | - python: "3.5" 14 | env: TOXENV=py35-django111 15 | - python: "3.6" 16 | env: TOXENV=py36-django111 17 | - python: "3.5" 18 | env: TOXENV=py35-django22 19 | - python: "3.6" 20 | env: TOXENV=py36-django22 21 | - python: "3.7" 22 | env: TOXENV=py37-django30 23 | - python: "3.6" 24 | env: TOXENV=py36-django30 25 | - python: "3.7" 26 | env: TOXENV=py37-django30 27 | - python: "3.8" 28 | env: TOXENV=py38-django30 29 | branches: 30 | only: 31 | - master 32 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [django-filebrowser.djangopo] 5 | file_filter = filebrowser/locale//LC_MESSAGES/django.po 6 | source_file = filebrowser/locale/en/LC_MESSAGES/django.po 7 | source_lang = en 8 | type = PO -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Patrick Kranzlmueller 2 | Axel Swoboda 3 | Klemens Mantzos 4 | Vaclav Mikolasek 5 | Tim Graham 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We are happy if you add tickets and help us improve the FileBrowser. 5 | However, in order to actually process tickets we need you to follow these guidelines. 6 | 7 | 1. Run Tests 8 | ------------ 9 | 10 | Before adding a ticket, please do run the tests:: 11 | 12 | python manage.py test filebrowser 13 | 14 | But be aware that some of the tests are copying files to the location of your storage engine. 15 | 16 | 2. Add Details 17 | -------------- 18 | 19 | If you add a ticket, you need to tell us which **versions of the FileBrowser, Grappelli and Django** you're using. 20 | Otherwise we won't be able to review the ticket. 21 | 22 | 3. Example 23 | ---------- 24 | 25 | Here's an example of a ticket we can easily review:: 26 | 27 | Tests: OK/ERROR (Details on errors) 28 | Version: FileBrowser x.x.x, Grappelli x.x.x, Django x.x 29 | 30 | Text of your ticket, as detailed as possible (code examples might help) 31 | 32 | 4. Issues 33 | --------- 34 | 35 | Please be aware that this is an issue tracker. If you're having questions, please us the `FileBrowser Google Group `_. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | django-filebrowser 2 | ------------------ 3 | 4 | Copyright (c) Patrick Kranzlmueller, Axel Swoboda (vonautomatisch werkstaetten), 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 3. Neither the name of FileBrowser nor the names of its contributors may be used 16 | to endorse or promote products derived from this software without specific prior 17 | written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 20 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 22 | THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README.rst 3 | include LICENSE 4 | recursive-include docs * 5 | recursive-include filebrowser/static * 6 | recursive-include filebrowser/templates * 7 | recursive-include filebrowser/locale * 8 | recursive-include filebrowser/management * 9 | recursive-include tests * 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django FileBrowser 2 | ================== 3 | .. image:: https://api.travis-ci.org/smacker/django-filebrowser-no-grappelli.svg 4 | :target: https://travis-ci.org/smacker/django-filebrowser-no-grappelli 5 | 6 | .. image:: https://readthedocs.org/projects/django-filebrowser/badge/?version=latest 7 | :target: http://django-filebrowser.readthedocs.org/en/latest/?badge=latest 8 | 9 | .. image:: https://img.shields.io/pypi/v/django-filebrowser-no-grappelli.svg 10 | :target: https://pypi.python.org/pypi/django-filebrowser-no-grappelli 11 | 12 | .. image:: https://img.shields.io/pypi/l/django-filebrowser-no-grappelli.svg 13 | :target: https://pypi.python.org/pypi/django-filebrowser-no-grappelli 14 | 15 | **Media-Management**. (based on https://github.com/sehmaschine/django-filebrowser) 16 | 17 | The FileBrowser is an extension to the `Django `_ administration interface in order to: 18 | 19 | * browse directories on your server and upload/delete/edit/rename files. 20 | * include images/documents to your models/database using the ``FileBrowseField``. 21 | * select images/documents with TinyMCE. 22 | 23 | Requirements 24 | ------------ 25 | 26 | FileBrowser 4.0 requires 27 | 28 | * Django 2/3/4 (http://www.djangoproject.com) 29 | * Pillow (https://github.com/python-imaging/Pillow) 30 | 31 | No Grappelli 32 | ------------ 33 | 34 | This fork removes the dependency on Grappelli. 35 | 36 | .. figure:: docs/_static/Screenshot.png 37 | :scale: 50 % 38 | :alt: django filebrowser no grappelli 39 | 40 | Installation 41 | ------------ 42 | 43 | Latest version: 44 | 45 | pip install -e git+git://github.com/smacker/django-filebrowser-no-grappelli.git#egg=django-filebrowser 46 | 47 | Stable version: 48 | 49 | pip install django-filebrowser-no-grappelli 50 | 51 | Documentation 52 | ------------- 53 | 54 | http://readthedocs.org/docs/django-filebrowser/ 55 | 56 | It also has fake model to show filebrowser in admin dashboard, but you can disable it by setting ``FILEBROWSER_SHOW_IN_DASHBOARD = False``. 57 | 58 | Translation 59 | ----------- 60 | 61 | https://www.transifex.com/projects/p/django-filebrowser/ 62 | 63 | Releases 64 | -------- 65 | 66 | * FileBrowser 4.0.2 (July 18th, 2023): Compatible with Django 3/4 67 | * FileBrowser 3.8.0 (November 4th, 2019): Compatible with Django 1.11/2.0/2.1/2.2/3.0 68 | * FileBrowser 3.7.9 (November 3rd, 2019): Compatible with Django 1.8/1.9/1.10/1.11/2.0/2.1/2.2 69 | * FileBrowser 3.6.2 (March 7th, 2016): Compatible with Django 1.4/1.5/1.6/1.7/1.8/1.9 70 | 71 | Older versions are available at GitHub, but are not supported anymore. 72 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoGrappelli.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoGrappelli.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoGrappelli" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoGrappelli" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_static/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/docs/_static/Screenshot.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. |grappelli| replace:: Grappelli 2 | .. |filebrowser| replace:: FileBrowser 3 | 4 | API 5 | === 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | filelisting 11 | fileobject -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 1 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _changelog: 7 | 8 | Changelog 9 | ========= 10 | 11 | 4.0.2 (July 18th, 2023) 12 | --------------------------- 13 | 14 | * Address Pillow 10 incompatibility. #80 Thanks to @christianwgd. 15 | 16 | 4.0.1 (Match 2nd, 2022) 17 | --------------------------- 18 | 19 | * Use unminified js files. #76 Thanks to @andylolz. 20 | 21 | 4.0.0 (February 2nd, 2022) 22 | --------------------------- 23 | 24 | * Django 4.0 support, drop Django <3. Thanks to @pulse-mind. 25 | * Support for TinyMCE v5. Thanks to @rmaceissoft. 26 | * Dropbox storage support. Thanks to @rmaceissoft. 27 | 28 | 3.8.0 (November 4th, 2019) 29 | --------------------------- 30 | 31 | * Django 3.0 support, drop Django <1.11. Thanks to @lpomfrey. 32 | 33 | 3.7.9 (November 3rd, 2019) 34 | --------------------------- 35 | 36 | * FileObject should implement os.PathLike protocol 37 | 38 | 3.7.8 (Match 25th, 2019) 39 | ------------------------ 40 | 41 | * Fix Chinese locale language code to match the one in Django 1.9+ 42 | 43 | 3.7.7 (February 2nd, 2019) 44 | -------------------------- 45 | 46 | * Fix of the previous release removing dependency on six 47 | 48 | 3.7.6 (February 1st, 2019) 49 | -------------------------- 50 | 51 | * Fix default value for FileBrowserField (thanks @carlosgeos for report) 52 | 53 | 3.7.5 (September 12th, 2018) 54 | ---------------------------- 55 | 56 | * Support for Django 2.1 (by @orbitvu) 57 | 58 | 3.7.4 (January 27th, 2018) 59 | -------------------------- 60 | 61 | * Fix manage dumpdata command for Django 2.0 (by @waustin) 62 | 63 | 3.7.3 (December 4th, 2017) 64 | -------------------------- 65 | 66 | * Compatibility with Django 2.0 67 | * Add config for use admin_custom 68 | 69 | 3.7.2 (Jun 8th, 2017) 70 | --------------------- 71 | 72 | * Fix AttributeError in Django 1.11 when saving 73 | 74 | 3.7.1 (May 28th, 2017) 75 | ---------------------- 76 | 77 | * New: Compatibility with Django 1.11 78 | * Folders being listed as selectable and 'format' not working correctly. (#29) 79 | * UI improvements 80 | 81 | 3.7.0 (Febrary 11th, 2017) 82 | -------------------------- 83 | 84 | * New: Compatibility with Django 1.10 85 | * Dropped support for Django < 1.8 86 | * Based on filebrowser v3.7.2 87 | 88 | 89 | For further information, see :ref:`releasenotes`. 90 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _faq: 7 | 8 | FAQ 9 | === 10 | 11 | Why should I use the |filebrowser|? 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 | If you need your editors or customers to manage files, the |filebrowser| is an alternative to an FTP-client. Moreover, you are able to define different image versions according to your websites grid. 15 | Alternatives to the |filebrowser| can be found at http://djangopackages.com/grids/g/file-managers/. 16 | 17 | Do I need |grappelli|? 18 | ^^^^^^^^^^^^^^^^^^^^^^ 19 | 20 | |grappelli| is a requirement for using the |filebrowser|. There are several filebrowser-no-grappelli repositories (most of them on GitHub), but we don't follow the development. 21 | 22 | I need help! 23 | ^^^^^^^^^^^^ 24 | 25 | see :ref:`Troubleshooting `. 26 | 27 | Why are there no fancy effects? 28 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | 30 | The |filebrowser| is about managing files. We think that you should prepare your files *before* uploading them to the server. 31 | 32 | How do I upload to another server? 33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 34 | 35 | Use a custom storage engine, see https://docs.djangoproject.com/en/1.9/howto/custom-file-storage/. 36 | 37 | Why do I need image-versions? 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | You need image-versions if your website is based on a *grid*. 41 | 42 | Is the |filebrowser| stable? 43 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 44 | 45 | We've developed the |filebrowser| for a couple of years and use it with almost all of our clients. That said, |grappelli| is the more stable and mature application. 46 | 47 | How can I contribute? 48 | ^^^^^^^^^^^^^^^^^^^^^ 49 | 50 | Help is very much needed and appreciated. Test the |filebrowser| and submit feedback/patches. 51 | 52 | Who develops the |filebrowser|? 53 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 54 | 55 | The |filebrowser| is developed and maintained by Patrick Kranzlmüller & Axel Swoboda of `vonautomatisch `_. 56 | -------------------------------------------------------------------------------- /docs/fieldswidgets.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | Fields & Widgets 7 | ================ 8 | 9 | The :ref:`filebrowsefield` is a custom model field which returns a :ref:`fileobject`. 10 | 11 | .. _filebrowsefield: 12 | 13 | FileBrowseField 14 | --------------- 15 | 16 | .. class:: FileBrowseField(max_length[, site, directory, extensions, format, **options]) 17 | 18 | A subclass of `CharField `_, referencing a media file within. 19 | Returns a :ref:`fileobject`. 20 | 21 | :param site: A FileBrowser site (defaults to the main site), see :ref:`site`. 22 | :param directory: Directory to browse when clicking the search icon. 23 | :param extensions: List of allowed extensions, see :ref:`settingsextensionsformats`. 24 | :param format: A key from SELECT_FORMATS in order to restrict the selection to specific filetypes, see :ref:`settingsextensionsformats`. 25 | 26 | For example: 27 | 28 | .. code-block:: python 29 | 30 | from filebrowser.fields import FileBrowseField 31 | 32 | class BlogEntry(models.Model): 33 | image = FileBrowseField("Image", max_length=200, directory="images/", extensions=[".jpg"], blank=True, null=True) 34 | document = FileBrowseField("PDF", max_length=200, directory="documents/", extensions=[".pdf",".doc"], blank=True, null=True) 35 | 36 | FileBrowseField in Templates 37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 38 | 39 | You can use all attributes from :ref:`fileobject`: 40 | 41 | .. code-block:: html 42 | 43 | {{ blogentry.image }} 44 | 45 | 46 | {% if blogentry.image.image_orientation == "landscape" %} 47 | 48 | {% endif %} 49 | 50 | Showing Thumbnail in the Changelist 51 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 52 | 53 | To show a thumbnail with the changelist, you can define a ModelAdmin method: 54 | 55 | .. code-block:: python 56 | 57 | from filebrowser.settings import ADMIN_THUMBNAIL 58 | 59 | def image_thumbnail(self, obj): 60 | if obj.image and obj.image.filetype == "Image": 61 | return '' % obj.image.version_generate(ADMIN_THUMBNAIL).url 62 | else: 63 | return "" 64 | image_thumbnail.allow_tags = True 65 | image_thumbnail.short_description = "Thumbnail" 66 | 67 | Using the FileBrowseField with TinyMCE 68 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 69 | 70 | In order to replace the TinyMCE image/file manager with the FileBrowser, you have to use a `FileBrowser Callback `_. There's an example TinyMCE configuration file in /static/js/ called TinyMCEAdmin.js. You can either copy the FileBrowserCallback to your own file or just use tinymce_setup.js (which comes with django-grappelli). 71 | 72 | Just add these lines to your `ModelAdmin asset definitions `_: 73 | 74 | .. code-block:: python 75 | 76 | class Media: 77 | js = ['/path/to/tinymce/jscripts/tiny_mce/tiny_mce.js', 78 | '/path/to/your/tinymce_setup.js'] 79 | 80 | Django FileField and the FileBrowser 81 | ------------------------------------ 82 | 83 | Return a :ref:`fileobject` from a `FileField `_ or `ImageField `_ with: 84 | 85 | .. code-block:: python 86 | 87 | from filebrowser.base import FileObject 88 | 89 | image_upload = models.ImageField(u"Image (Upload)", max_length=250, upload_to=image_upload_path, blank=True, null=True) 90 | 91 | def image(self): 92 | if self.image_upload: 93 | return FileObject(self.image_upload.path) 94 | return None 95 | 96 | In order show a thumbnail with your changelist, you could use a ModelAdmin method: 97 | 98 | .. code-block:: python 99 | 100 | from filebrowser.base import FileObject 101 | 102 | def image_thumbnail(self, obj): 103 | if obj.image_upload: 104 | image = FileObject(obj.image_upload.path) 105 | if image.filetype == "Image": 106 | return '' % image.version_generate(ADMIN_THUMBNAIL).url 107 | else: 108 | return "" 109 | image_thumbnail.allow_tags = True 110 | image_thumbnail.short_description = "Thumbnail" 111 | 112 | .. note:: 113 | There are different ways to achieve this. The above examples show one of several options. 114 | -------------------------------------------------------------------------------- /docs/filelisting.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 3 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _filelisting: 7 | 8 | FileListing 9 | =========== 10 | 11 | .. class:: FileListing(path, filter_func=None, sorting_by=None, sorting_order=None) 12 | 13 | Returns a list of FileObjects for a server path, see :ref:`fileobject`. 14 | 15 | :param path: Relative path to a location within `site.storage.location`. 16 | :param filter_func: Filter function, see example below. 17 | :param sorting_by: Sort the files by any attribute of FileObject. 18 | :param sorting_order: Sorting order, either "asc" or "desc". 19 | 20 | If you want to list all files within a storage location you do: 21 | 22 | .. code-block:: python 23 | 24 | from filebrowser.sites import site 25 | from filebrowser.base import FileListing 26 | filelisting = FileListing(site.storage.location, sorting_by='date', sorting_order='desc') 27 | 28 | Use a custom filter function to limit the list of files: 29 | 30 | .. code-block:: python 31 | 32 | def filter_filelisting(item): 33 | # item is a FileObject 34 | return item.filetype != "Folder" 35 | 36 | filelisting = FileListing(site.storage.location, filter_func=filter_listing, sorting_by='date', sorting_order='desc') 37 | 38 | Methods 39 | ------- 40 | 41 | For the below examples, we're using this folder-structure.:: 42 | 43 | /media/uploads/testfolder/testimage.jpg 44 | /media/uploads/blog/1/images/blogimage.jpg 45 | 46 | .. note:: 47 | We defined ``filter_browse`` as ``filter_func`` (see sites.py). And we did not define a ``VERSIONS_BASEDIR`` for this demonstration, though it is highly recommended to use one. 48 | 49 | .. method:: listing() 50 | 51 | Returns all items for the given path with ``os.listdir(path)``:: 52 | 53 | >>> for item in filelisting.listing(): 54 | ... print item 55 | blog 56 | testfolder 57 | 58 | .. method:: walk() 59 | 60 | Returns all items for the given path with ``os.walk(path)``:: 61 | 62 | >>> for item in filelisting.walk(): 63 | ... print item 64 | blog 65 | blog/1 66 | blog/1/images 67 | blog/1/images/blogimage.jpg 68 | blog/1/images/blogimage_admin_thumbnail.jpg 69 | blog/1/images/blogimage_medium.jpg 70 | blog/1/images/blogimage_small.jpg 71 | blog/1/images/blogimage_thumbnail.jpg 72 | testfolder 73 | testfolder/testimage.jpg 74 | 75 | .. method:: files_listing_total() 76 | 77 | Returns a sorted list of ``FileObjects`` for :meth:`listing()`:: 78 | 79 | >>> for item in filelisting.files_listing_total(): 80 | ... print item 81 | uploads/blog/ 82 | uploads/testfolder/ 83 | 84 | .. method:: files_walk_total() 85 | 86 | Returns a sorted list of ``FileObjects`` for :meth:`walk()`:: 87 | 88 | >>> for item in filelisting.files_walk_total(): 89 | ... print item 90 | uploads/blog/ 91 | uploads/blog/1/ 92 | uploads/blog/1/images/ 93 | uploads/blog/1/images/blogimage.jpg 94 | uploads/blog/1/images/blogimage_admin_thumbnail.jpg 95 | uploads/blog/1/images/blogimage_medium.jpg 96 | uploads/blog/1/images/blogimage_small.jpg 97 | uploads/blog/1/images/blogimage_thumbnail.jpg 98 | uploads/testfolder/ 99 | uploads/testfolder/testimage.jpg 100 | 101 | .. method:: files_listing_filtered() 102 | 103 | Returns a sorted and filtered list of ``FileObjects`` for :meth:`listing()`:: 104 | 105 | >>> for item in filelisting.files_listing_filtered(): 106 | ... print item 107 | uploads/blog/ 108 | uploads/testfolder/ 109 | 110 | .. method:: files_walk_filtered() 111 | 112 | Returns a sorted and filtered list of ``FileObjects`` for :meth:`walk()`:: 113 | 114 | >>> for item in filelisting.files_walk_filtered(): 115 | ... print item 116 | uploads/blog/ 117 | uploads/blog/1/ 118 | uploads/blog/1/images/ 119 | uploads/blog/1/images/blogimage.jpg 120 | uploads/testfolder/ 121 | uploads/testfolder/testimage.jpg 122 | 123 | .. note:: 124 | The versions are not listed (compared with files_walk_total) because of filter_func. 125 | 126 | .. method:: results_listing_total() 127 | 128 | Number of total files, based on :meth:`files_listing_total()`:: 129 | 130 | >>> filelisting.results_listing_total() 131 | 2 132 | 133 | .. method:: results_walk_total() 134 | 135 | Number of total files, based on :meth:`files_walk_total()`:: 136 | 137 | >>> filelisting.results_walk_total() 138 | 10 139 | 140 | .. method:: results_listing_filtered() 141 | 142 | Number of filtered files, based on :meth:`files_listing_filtered()`:: 143 | 144 | >>> filelisting.results_listing_filtered() 145 | 2 146 | 147 | .. method:: results_walk_filtered() 148 | 149 | Number of filtered files, based on :meth:`files_walk_filtered()`:: 150 | 151 | >>> filelisting.results_walk_filtered() 152 | 6 153 | -------------------------------------------------------------------------------- /docs/help.rst: -------------------------------------------------------------------------------- 1 | .. |grappelli| replace:: Grappelli 2 | .. |filebrowser| replace:: FileBrowser 3 | 4 | Help 5 | ==== 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | faq 11 | troubleshooting 12 | translation 13 | releasenotes -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django FileBrowser documentation master file, created by 2 | sphinx-quickstart on Sun Dec 5 19:11:46 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | .. |grappelli| replace:: Grappelli 6 | .. |filebrowser| replace:: FileBrowser 7 | 8 | Django FileBrowser Documentation 9 | ================================ 10 | 11 | **Media-Management with Grappelli**. 12 | 13 | .. note:: 14 | |filebrowser| 3.7.2 requires Django 1.9 and |grappelli| 2.8. 15 | 16 | Installation and Setup 17 | ---------------------- 18 | 19 | .. toctree:: 20 | :maxdepth: 3 21 | 22 | quickstart 23 | settings 24 | 25 | API 26 | --- 27 | 28 | .. toctree:: 29 | :maxdepth: 4 30 | 31 | api 32 | 33 | Fields & Widgets 34 | ---------------- 35 | 36 | .. toctree:: 37 | :maxdepth: 3 38 | 39 | fieldswidgets 40 | 41 | Admin Interface 42 | --------------- 43 | 44 | .. toctree:: 45 | :maxdepth: 3 46 | 47 | admin 48 | 49 | Image Versions 50 | -------------- 51 | 52 | .. toctree:: 53 | :maxdepth: 3 54 | 55 | versions 56 | 57 | Help 58 | ---- 59 | 60 | .. toctree:: 61 | :maxdepth: 2 62 | 63 | help 64 | testing 65 | changelog 66 | 67 | Main Features 68 | ------------- 69 | 70 | * Browse your media files with the admin interface. 71 | * Multiple upload, including a progress bar. 72 | * Automatic thumbnails. 73 | * Image versions to fit your websites grid (esp. useful with adaptive/responsive layouts). 74 | * Integration with TinyMCE. 75 | * FileBrowseField to select images/documents. 76 | * Signals for upload, rename and delete. 77 | * Custom actions. 78 | * Custom file storage engines. 79 | 80 | Code 81 | ---- 82 | 83 | https://github.com/sehmaschine/django-filebrowser 84 | 85 | Discussion 86 | ---------- 87 | 88 | Use the `FileBrowser Google Group `_ to ask questions or discuss features. 89 | 90 | Versions and Compatibility 91 | -------------------------- 92 | 93 | **FileBrowser is always developed against the latest stable Django release and is NOT tested with Djangos trunk.** 94 | 95 | * FileBrowser 3.7.2 (August 9th, 2016): Compatible with Django 1.9 96 | * FileBrowser 3.6.4 (March 6th, 2016): Compatible with Django 1.8 97 | * FileBrowser 3.5.8 (September 7th, 2015): Compatible with Django 1.4/1.5/1.6/1.7 98 | 99 | Current development branches: 100 | 101 | * FileBrowser 3.7.3 (Development Version for Django = 1.9, see Branch Stable/3.7.x) 102 | * FileBrowser 3.6.5 (Development Version for Django = 1.8, see Branch Stable/3.6.x) 103 | * FileBrowser 3.5.9 (Development Version for Django <= 1.7, see Branch Stable/3.5.x) 104 | 105 | Older versions are available at GitHub, but are not supported anymore. 106 | Support for 3.5.x and 3.6.x is limited to security issues and very important bugfixes. 107 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. |grappelli| replace:: Grappelli 2 | .. |filebrowser| replace:: FileBrowser 3 | 4 | .. _quickstart: 5 | 6 | Quick start guide 7 | ================= 8 | 9 | For using the |filebrowser|, `Django `_ needs to be installed and an `Admin Site `_ has to be activated. 10 | 11 | Requirements 12 | ------------ 13 | 14 | * Django 1.9, http://www.djangoproject.com 15 | * Grappelli 2.8, https://github.com/sehmaschine/django-grappelli 16 | * Pillow, https://github.com/python-imaging/Pillow 17 | 18 | Installation 19 | ------------ 20 | 21 | Install the |filebrowser|: 22 | 23 | .. code-block:: console 24 | 25 | pip install django-filebrowser 26 | 27 | Add the filebrowser to your ``INSTALLED_APPS`` (before django.contrib.admin): 28 | 29 | .. code-block:: python 30 | 31 | INSTALLED_APPS = ( 32 | 'grappelli', 33 | 'filebrowser', 34 | 'django.contrib.admin', 35 | ) 36 | 37 | Add the |filebrowser| site to your url-patterns (before any admin-urls): 38 | 39 | .. code-block:: python 40 | 41 | from filebrowser.sites import site 42 | 43 | urlpatterns = [ 44 | re_path(r'^admin/filebrowser/', include(site.urls)), 45 | re_path(r'^grappelli/', include('grappelli.urls')), 46 | re_path(r'^admin/', include(admin.site.urls)), 47 | ] 48 | 49 | Collect the static files (please refer to the `Staticfiles Documentation `_ for more information): 50 | 51 | .. code-block:: console 52 | 53 | python manage.py collectstatic 54 | 55 | Settings 56 | -------- 57 | 58 | Check the :ref:`settings`. 59 | 60 | .. note:: 61 | You need to add a folder "uploads" within ``site.storage.location`` when using the default settings. 62 | 63 | Testing 64 | ------- 65 | 66 | Start the devserver and login to your admin site: 67 | 68 | .. code-block:: console 69 | 70 | python manage.py runserver :8000 71 | 72 | Goto /admin/filebrowser/browse/ and check if everything looks/works as expected. If you're having problems, see :ref:`troubleshooting`. 73 | -------------------------------------------------------------------------------- /docs/releasenotes.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _releasenotes: 7 | 8 | FileBrowser 3.7 Release Notes 9 | ============================= 10 | 11 | FileBrowser 3.7 is compatible with Django 1.9 as well as Grappelli 2.8. 12 | 13 | Updates 14 | ------- 15 | 16 | * Compatibility with Django 1.9 and Grappelli 2.8 17 | * If you use versions, defining a ``VERSIONS_BASEDIR`` outside of site.directory is now mandatory. 18 | 19 | Depreciations (3.7) 20 | ------------------- 21 | 22 | As already noted with 3.6, there's a couple of backwards-incompatible changes with 3.7. 23 | 24 | * FileObject ``directory`` is deprecated (use ``path_relative_directory`` instead). 25 | * FileObject ``folder`` is deprecated (use ``dirname`` instead). 26 | * ``FileInput`` and ``ClearableFileInput`` have been removed. 27 | * ``version_object`` has been removed (use ``version`` instead). 28 | 29 | Update from FileBrowser 3.6.x 30 | ----------------------------- 31 | 32 | * Update Django to 1.9 and check https://docs.djangoproject.com/en/dev/releases/1.8/ 33 | * Update Grappelli to 2.8.x 34 | * Update FileBrowser to 3.7.x 35 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 1 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _changelog: 7 | 8 | Testing 9 | ======= 10 | 11 | Filebrowser is shipped with a minimal django project for testing. 12 | 13 | Run the |filebrowser| tests: 14 | 15 | .. code-block:: console 16 | 17 | tox 18 | 19 | .. warning:: 20 | Please note that the tests will copy files to your filesystem. 21 | 22 | Travis 23 | ------ 24 | 25 | See https://travis-ci.org/sehmaschine/django-filebrowser. 26 | -------------------------------------------------------------------------------- /docs/translation.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _translation: 7 | 8 | Translation 9 | =========== 10 | 11 | Translation is done via `Transifex `_. 12 | 13 | Supported Languages 14 | ------------------- 15 | 16 | see https://www.transifex.net/projects/p/django-filebrowser/resource/djangopo/ -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _troubleshooting: 7 | 8 | Troubleshooting 9 | =============== 10 | 11 | Check your setup 12 | ^^^^^^^^^^^^^^^^ 13 | 14 | Please check if the problem is caused by your setup. 15 | 16 | * Read :ref:`quickstart`. 17 | * Check if the static/media-files are served correctly. 18 | * Make sure you have removed all custom |filebrowser| templates from all locations in ``TEMPLATE_DIRS`` or check that these templates are compatible with the |filebrowser|. 19 | 20 | Run the tests 21 | ^^^^^^^^^^^^^ 22 | 23 | Start the shell and type: 24 | 25 | .. code-block:: console 26 | 27 | python manage.py test filebrowser 28 | 29 | .. warning:: 30 | Please note that the tests will copy files to your filesystem. 31 | 32 | Check issues 33 | ^^^^^^^^^^^^ 34 | 35 | If your setup is fine, please check if your problem is a known issue. 36 | 37 | * Take a look at all `FileBrowser Issues `_ (incuding closed) and search the `FileBrowser Google-Group `_. 38 | 39 | Add a ticket 40 | ^^^^^^^^^^^^ 41 | 42 | If you think you've found a bug, please `add a ticket `_. 43 | 44 | * Try to describe your problem as precisely as possible. 45 | * Tell us what you did in order to solve the problem. 46 | * Tell us what version of the |filebrowser| you are using. 47 | * Tell us what version of Django you are using. 48 | * Please do NOT add tickets if you're having problems with serving static/media-files (because this is not related to the |filebrowser|). 49 | * Please do NOT add tickets referring to Djangos trunk version. 50 | * At best: add a patch. 51 | 52 | .. note:: 53 | Be aware that we may close issues not following these guidlines without further notifications. -------------------------------------------------------------------------------- /docs/versions.rst: -------------------------------------------------------------------------------- 1 | .. :tocdepth: 1 2 | 3 | .. |grappelli| replace:: Grappelli 4 | .. |filebrowser| replace:: FileBrowser 5 | 6 | .. _versions: 7 | 8 | Versions 9 | ======== 10 | 11 | With the FileBrowser, you are able to define different versions/sizes for images. This enables you to save an original image on your server while having different versions of that image to automatically fit your websites grid. Versions are also useful for responsive/adaptive layouts. 12 | 13 | To generate a version of a source image, you specify `options` which are used 14 | by the image processors (see :ref:`settingsversions_processors`) to generate the 15 | required version. 16 | 17 | Defining Versions 18 | ----------------- 19 | 20 | First you need to know which versions/sizes of an image you'd like to generate 21 | with your website. Let's say you're using a 12 column grid with 60px for each 22 | column and 20px margin (which is a total of 940px). With this grid, you could 23 | (for example) define these image :ref:`settingsversions_versions`: 24 | 25 | .. code-block:: python 26 | 27 | FILEBROWSER_VERSIONS_BASEDIR = '_versions' 28 | FILEBROWSER_VERSIONS = { 29 | 'admin_thumbnail': {'verbose_name': 'Admin Thumbnail', 'width': 60, 'height': 60, 'opts': 'crop'}, 30 | 'thumbnail': {'verbose_name': 'Thumbnail (1 col)', 'width': 60, 'height': 60, 'opts': 'crop'}, 31 | 'small': {'verbose_name': 'Small (2 col)', 'width': 140, 'height': '', 'opts': ''}, 32 | 'medium': {'verbose_name': 'Medium (4col )', 'width': 300, 'height': '', 'opts': ''}, 33 | 'big': {'verbose_name': 'Big (6 col)', 'width': 460, 'height': '', 'opts': ''}, 34 | 'large': {'verbose_name': 'Large (8 col)', 'width': 680, 'height': '', 'opts': ''}, 35 | } 36 | 37 | Use the ``methods`` argument, if you need to add a filter: 38 | 39 | .. code-block:: python 40 | 41 | def grayscale(im): 42 | "Convert image to grayscale" 43 | if im.mode != "L": 44 | im = im.convert("L") 45 | return im 46 | 47 | FILEBROWSER_VERSIONS = { 48 | 'big': {'verbose_name': 'Big (6 col)', 'width': 460, 'height': '', 'opts': '', 'methods': [grayscale]}, 49 | }) 50 | 51 | 52 | .. _versions__custom_processors: 53 | 54 | Custom processors 55 | ----------------- 56 | 57 | .. versionadded:: 3.7.2 58 | 59 | Custom processors can be created using a simple method like this: 60 | 61 | .. code:: python 62 | 63 | def grayscale_processor(im, grayscale=False, **kwargs): 64 | if grayscale: 65 | if im.mode != "L": 66 | im = im.convert("L") 67 | return im 68 | 69 | The first argument for a processor is the source image. 70 | 71 | All other arguments are keyword arguments which relate to the list of options 72 | received from the :ref:`version_generate method `. 73 | 74 | Ensure that you explicitly declare all params that could be used by your 75 | processor, as the processors arguments can be inspected to get a list of valid 76 | options. 77 | 78 | In order to turn your processor optional, define the params that your processor 79 | expects with a falsy default, and in this case you could return the 80 | original image without any modification. 81 | 82 | You must also use ``**kwargs`` at the end of your argument list because all 83 | `options` used to generate the version are available to all processors, not 84 | just the ones defined in your processor. 85 | 86 | Whether a processor actually modifies the image or not, they must always return 87 | an image. 88 | 89 | Using the processor 90 | +++++++++++++++++++ 91 | 92 | Override the :ref:`settingsversions_processors` setting: 93 | 94 | .. code-block:: python 95 | 96 | FILEBROWSER_VERSION_PROCESSORS = [ 97 | 'filebrowser.utils.scale_and_crop', 98 | 'my_project.my_processors.grayscale_processor', 99 | ] 100 | 101 | And in your versions definition: 102 | 103 | .. code-block:: python 104 | 105 | FILEBROWSER_VERSIONS = { 106 | 'big_gray': {'verbose_name': 'Big (6 col)', 'width': 460, 'grayscale': True}, 107 | }) 108 | 109 | 110 | Versions and the Admin 111 | ---------------------- 112 | 113 | When using the FileBrowser with the admin interface, you need to define ``ADMIN_VERSIONS`` and ``ADMIN_THUMBNAIL`` (see :ref:`settings`). ``ADMIN_VERSIONS`` are available with the admin, i.e. you are able to see these versions with the image detail view and you are able to select the versions with the :ref:`filebrowsefield` model field. 114 | 115 | .. code-block:: python 116 | 117 | FILEBROWSER_ADMIN_VERSIONS = ['thumbnail', 'small', 'medium', 'big', 'large'] 118 | FILEBROWSER_ADMIN_THUMBNAIL = 'admin_thumbnail' 119 | 120 | Versions and the Frontend 121 | ------------------------- 122 | 123 | With the templatetag ``version`` a version will be generated if it doesn't already exist OR if the original image is newer than the version. 124 | In order to update an image, you just overwrite the original image and the versions will be generated automatically (as you request them within your template). 125 | 126 | A Model example: 127 | 128 | .. code-block:: python 129 | 130 | from filebrowser.fields import FileBrowseField 131 | 132 | class BlogEntry(models.Model): 133 | image = FileBrowseField("Image", max_length=200, blank=True, null=True) 134 | 135 | With your templates, use ``version`` if you simply need to retrieve the URL or ``version as var`` if you need to get a :ref:`fileobject`: 136 | 137 | .. code-block:: html 138 | 139 | 140 | {% load fb_versions %} 141 | 142 | 143 | 144 | 145 | 146 | {% version blogentry.image 'medium' as version_medium %} 147 | {{ version_medium.width }} 148 | 149 | 150 | Templatetag ``version`` 151 | +++++++++++++++++++++++ 152 | 153 | Retrieves/Generates a version and returns an URL: 154 | 155 | .. code-block:: html 156 | 157 | {% version model.field_name version_prefix %} 158 | 159 | Retrieves/Generates a version and returns a FileObject: 160 | 161 | .. code-block:: html 162 | 163 | {% version model.field_name version_prefix as variable %} 164 | 165 | .. note:: 166 | ``version_prefix`` can either be a string or a variable. If ``version_prefix`` is a string, use quotes. 167 | 168 | Versions in Views 169 | ----------------- 170 | 171 | If you have a ``FileObject`` you can generate/retrieve a version with: 172 | 173 | .. code-block:: python 174 | 175 | v = obj.image.version_generate(version_prefix) # returns a FileObject 176 | 177 | Placeholder 178 | ----------- 179 | 180 | When developing on a locale machine or a development-server, you might not have all the images (resp. media-files) available that are on your production instance and downloading these files on a regular basis might not be an option. 181 | 182 | In that case, you can use a placeholder instead of a version. You just need to define the ``PLACEHOLDER`` and overwrite the settings ``SHOW_PLACEHOLDER`` and/or ``FORCE_PLACEHOLDER`` (see :ref:`settingsplaceholder`). 183 | 184 | Management Commands 185 | ------------------- 186 | 187 | .. option:: fb_version_generate 188 | 189 | If you need to generate certain (or all) versions, type: 190 | 191 | .. code-block:: python 192 | 193 | python manage.py fb_version_generate 194 | 195 | .. option:: fb_version_remove 196 | 197 | If you need to remove certain (or all) versions, type: 198 | 199 | .. code-block:: python 200 | 201 | python manage.py fb_version_remove 202 | 203 | .. warning:: 204 | Please be very careful with this command. 205 | -------------------------------------------------------------------------------- /filebrowser/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '4.0.2' 2 | -------------------------------------------------------------------------------- /filebrowser/actions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import tempfile 5 | 6 | from django.contrib import messages 7 | from django.core.files import File 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from filebrowser.settings import VERSION_QUALITY, STRICT_PIL 11 | 12 | if STRICT_PIL: 13 | from PIL import Image 14 | else: 15 | try: 16 | from PIL import Image 17 | except ImportError: 18 | import Image 19 | 20 | 21 | def applies_to_all_images(fileobject): 22 | "Set image filetype" 23 | return fileobject.filetype == 'Image' 24 | 25 | 26 | def transpose_image(request, fileobjects, operation): 27 | "Transpose image" 28 | for fileobject in fileobjects: 29 | root, ext = os.path.splitext(fileobject.filename) 30 | f = fileobject.site.storage.open(fileobject.path) 31 | im = Image.open(f) 32 | new_image = im.transpose(operation) 33 | tmpfile = File(tempfile.NamedTemporaryFile()) 34 | 35 | try: 36 | new_image.save(tmpfile, format=Image.EXTENSION[ext], quality=VERSION_QUALITY, optimize=(os.path.splitext(fileobject.path)[1].lower() != '.gif')) 37 | except IOError: 38 | new_image.save(tmpfile, format=Image.EXTENSION[ext], quality=VERSION_QUALITY) 39 | 40 | try: 41 | saved_under = fileobject.site.storage.save(fileobject.path, tmpfile) 42 | if saved_under != fileobject.path: 43 | fileobject.site.storage.move(saved_under, fileobject.path, allow_overwrite=True) 44 | fileobject.delete_versions() 45 | finally: 46 | tmpfile.close() 47 | f.close() 48 | 49 | messages.add_message(request, messages.SUCCESS, _("Action applied successfully to '%s'" % (fileobject.filename))) 50 | 51 | 52 | def flip_horizontal(request, fileobjects): 53 | "Flip image horizontally" 54 | transpose_image(request, fileobjects, 0) 55 | flip_horizontal.short_description = _(u'Flip horizontal') 56 | flip_horizontal.applies_to = applies_to_all_images 57 | 58 | 59 | def flip_vertical(request, fileobjects): 60 | "Flip image vertically" 61 | transpose_image(request, fileobjects, 1) 62 | flip_vertical.short_description = _(u'Flip vertical') 63 | flip_vertical.applies_to = applies_to_all_images 64 | 65 | 66 | def rotate_90_clockwise(request, fileobjects): 67 | "Rotate image 90 degrees clockwise" 68 | transpose_image(request, fileobjects, 4) 69 | rotate_90_clockwise.short_description = _(u'Rotate 90° CW') 70 | rotate_90_clockwise.applies_to = applies_to_all_images 71 | 72 | 73 | def rotate_90_counterclockwise(request, fileobjects): 74 | "Rotate image 90 degrees counterclockwise" 75 | transpose_image(request, fileobjects, 2) 76 | rotate_90_counterclockwise.short_description = _(u'Rotate 90° CCW') 77 | rotate_90_counterclockwise.applies_to = applies_to_all_images 78 | 79 | 80 | def rotate_180(request, fileobjects): 81 | "Rotate image 180 degrees" 82 | transpose_image(request, fileobjects, 3) 83 | rotate_180.short_description = _(u'Rotate 180°') 84 | rotate_180.applies_to = applies_to_all_images 85 | -------------------------------------------------------------------------------- /filebrowser/admin.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from django.contrib import admin 3 | try: 4 | from django.urls import reverse 5 | except ImportError: 6 | from django.core.urlresolvers import reverse 7 | from django.http import HttpResponseRedirect 8 | from .models import FileBrowser 9 | from .settings import SHOW_IN_DASHBOARD 10 | 11 | 12 | class FileBrowserAdmin(admin.ModelAdmin): 13 | actions = [] 14 | 15 | def has_add_permission(self, request): 16 | return False 17 | 18 | def has_delete_permission(self, request, obj=None): 19 | return False 20 | 21 | def get_urls(self): 22 | opts = self.model._meta 23 | info = opts.app_label, (opts.model_name if hasattr(opts, 'model_name') else opts.module_name) 24 | return [ 25 | re_path('^$', self.admin_site.admin_view(self.filebrowser_view), name='{0}_{1}_changelist'.format(*info)), 26 | ] 27 | 28 | def filebrowser_view(self, request): 29 | return HttpResponseRedirect(reverse('filebrowser:fb_browse')) 30 | 31 | 32 | if SHOW_IN_DASHBOARD: 33 | admin.site.register(FileBrowser, FileBrowserAdmin) 34 | -------------------------------------------------------------------------------- /filebrowser/compat.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | def get_modified_time(storage, path): 4 | if hasattr(storage, "get_modified_time"): 5 | return storage.get_modified_time(path) 6 | return storage.modified_time(path) 7 | -------------------------------------------------------------------------------- /filebrowser/decorators.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | from django.contrib import messages 6 | from django.core.exceptions import ImproperlyConfigured 7 | try: 8 | from django.urls import reverse 9 | except ImportError: 10 | from django.core.urlresolvers import reverse 11 | from django.http import HttpResponseRedirect 12 | from django.utils.encoding import smart_str 13 | from django.utils.translation import gettext as _ 14 | 15 | from filebrowser.templatetags.fb_tags import query_helper 16 | 17 | 18 | def get_path(path, site): 19 | converted_path = smart_str(os.path.join(site.directory, path)) 20 | if not path.startswith('.') and not os.path.isabs(converted_path): 21 | if site.storage.isdir(converted_path): 22 | return path 23 | 24 | 25 | def get_file(path, filename, site): 26 | # Files and directories are valid 27 | converted_path = smart_str(os.path.join(site.directory, path, filename)) 28 | if not path.startswith('.') and not filename.startswith('.') and not os.path.isabs(converted_path): 29 | if site.storage.isfile(converted_path) or site.storage.isdir(converted_path): 30 | return filename 31 | 32 | 33 | def path_exists(site, function): 34 | "Check if the given path exists." 35 | 36 | def decorator(request, *args, **kwargs): 37 | # TODO: This check should be moved to a better location than a decorator 38 | if get_path('', site=site) is None: 39 | # The storage location does not exist, raise an error to prevent eternal redirecting. 40 | raise ImproperlyConfigured(_("Error finding Upload-Folder (site.storage.location + site.directory). Maybe it does not exist?")) 41 | if get_path(request.GET.get('dir', ''), site=site) is None: 42 | msg = _('The requested Folder does not exist.') 43 | messages.add_message(request, messages.ERROR, msg) 44 | redirect_url = reverse("filebrowser:fb_browse", current_app=site.name) + query_helper(request.GET, u"", "dir") 45 | return HttpResponseRedirect(redirect_url) 46 | return function(request, *args, **kwargs) 47 | return decorator 48 | 49 | 50 | def file_exists(site, function): 51 | "Check if the given file exists." 52 | 53 | def decorator(request, *args, **kwargs): 54 | file_path = get_file(request.GET.get('dir', ''), request.GET.get('filename', ''), site=site) 55 | if file_path is None: 56 | msg = _('The requested File does not exist.') 57 | messages.add_message(request, messages.ERROR, msg) 58 | redirect_url = reverse("filebrowser:fb_browse", current_app=site.name) + query_helper(request.GET, u"", "dir") 59 | return HttpResponseRedirect(redirect_url) 60 | return function(request, *args, **kwargs) 61 | return decorator 62 | -------------------------------------------------------------------------------- /filebrowser/forms.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import re 5 | 6 | from django import forms 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from filebrowser.settings import FOLDER_REGEX 10 | from filebrowser.utils import convert_filename 11 | 12 | 13 | ALNUM_NAME_RE = re.compile(FOLDER_REGEX, re.U) 14 | 15 | TRANSPOSE_CHOICES = ( 16 | ("", u"-----"), 17 | ("0", _(u"Flip horizontal")), 18 | ("1", _(u"Flip vertical")), 19 | ("2", _(u"Rotate 90° CW")), 20 | ("4", _(u"Rotate 90° CCW")), 21 | ("3", _(u"Rotate 180°")), 22 | ) 23 | 24 | 25 | class CreateDirForm(forms.Form): 26 | """ 27 | Form for creating a folder. 28 | """ 29 | 30 | name = forms.CharField(widget=forms.TextInput(attrs=dict({'class': 'vTextField'}, max_length=50, min_length=3)), label=_(u'Name'), help_text=_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True) 31 | 32 | def __init__(self, path, *args, **kwargs): 33 | self.path = path 34 | self.site = kwargs.pop("filebrowser_site", None) 35 | super(CreateDirForm, self).__init__(*args, **kwargs) 36 | 37 | def clean_name(self): 38 | "validate name" 39 | if self.cleaned_data['name']: 40 | # only letters, numbers, underscores, spaces and hyphens are allowed. 41 | if not ALNUM_NAME_RE.search(self.cleaned_data['name']): 42 | raise forms.ValidationError(_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.')) 43 | # Folder must not already exist. 44 | if self.site.storage.isdir(os.path.join(self.path, convert_filename(self.cleaned_data['name']))): 45 | raise forms.ValidationError(_(u'The Folder already exists.')) 46 | return convert_filename(self.cleaned_data['name']) 47 | 48 | 49 | class ChangeForm(forms.Form): 50 | """ 51 | Form for renaming a file/folder. 52 | """ 53 | 54 | custom_action = forms.ChoiceField(label=_(u'Actions'), required=False) 55 | name = forms.CharField(widget=forms.TextInput(attrs=dict({'class': 'vTextField'}, max_length=50, min_length=3)), label=_(u'Name'), help_text=_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True) 56 | 57 | def __init__(self, *args, **kwargs): 58 | self.path = kwargs.pop("path", None) 59 | self.fileobject = kwargs.pop("fileobject", None) 60 | self.site = kwargs.pop("filebrowser_site", None) 61 | super(ChangeForm, self).__init__(*args, **kwargs) 62 | 63 | # Initialize choices of custom action 64 | choices = [("", u"-----")] 65 | for name, action in self.site.applicable_actions(self.fileobject): 66 | choices.append((name, action.short_description)) 67 | self.fields['custom_action'].choices = choices 68 | 69 | def clean_name(self): 70 | "validate name" 71 | if self.cleaned_data['name']: 72 | # only letters, numbers, underscores, spaces and hyphens are allowed. 73 | if not ALNUM_NAME_RE.search(self.cleaned_data['name']): 74 | raise forms.ValidationError(_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.')) 75 | # folder/file must not already exist. 76 | if self.site.storage.isdir(os.path.join(self.path, convert_filename(self.cleaned_data['name']))) and os.path.join(self.path, convert_filename(self.cleaned_data['name'])) != self.fileobject.path: 77 | raise forms.ValidationError(_(u'The Folder already exists.')) 78 | elif self.site.storage.isfile(os.path.join(self.path, convert_filename(self.cleaned_data['name']))) and os.path.join(self.path, convert_filename(self.cleaned_data['name'])) != self.fileobject.path: 79 | raise forms.ValidationError(_(u'The File already exists.')) 80 | return convert_filename(self.cleaned_data['name']) 81 | -------------------------------------------------------------------------------- /filebrowser/locale/az/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/az/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/bg_BG/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/bg_BG/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/is/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/is/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/ja/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/ja/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/ja_JP/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/ja_JP/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/mn/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/mn/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/pl_PL/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/pl_PL/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/ru_RU/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/ru_RU/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/sk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/sk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/sl_SI/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/sl_SI/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/sv/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/sv/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/tr_TR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/tr_TR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/vi/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/vi/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /filebrowser/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /filebrowser/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /filebrowser/management/commands/fb_version_generate.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import re 5 | 6 | from django.conf import settings 7 | from django.core.management.base import BaseCommand, CommandError 8 | from six.moves import input 9 | 10 | from filebrowser.base import FileListing 11 | from filebrowser.settings import EXTENSION_LIST, EXCLUDE, DIRECTORY, VERSIONS 12 | 13 | 14 | filter_re = [] 15 | for exp in EXCLUDE: 16 | filter_re.append(re.compile(exp)) 17 | for k, v in VERSIONS.items(): 18 | exp = (r'_%s(%s)') % (k, '|'.join(EXTENSION_LIST)) 19 | filter_re.append(re.compile(exp)) 20 | 21 | 22 | class Command(BaseCommand): 23 | help = "(Re)Generate image versions." 24 | 25 | def add_arguments(self, parser): 26 | parser.add_argument('media_path', nargs='?', default=DIRECTORY) 27 | 28 | def handle(self, *args, **options): 29 | path = options['media_path'] 30 | 31 | if not os.path.isdir(os.path.join(settings.MEDIA_ROOT, path)): 32 | raise CommandError(' must be a directory in MEDIA_ROOT (If you don\'t add a media_path the default path is DIRECTORY).\n"%s" is no directory.' % path) 33 | 34 | # get version name 35 | while 1: 36 | self.stdout.write('\nSelect a version you want to generate:\n') 37 | for version in VERSIONS: 38 | self.stdout.write(' * %s\n' % version) 39 | 40 | version_name = input('(leave blank to generate all versions): ') 41 | 42 | if version_name == "": 43 | selected_version = None 44 | break 45 | else: 46 | try: 47 | tmp = VERSIONS[version_name] 48 | selected_version = version_name 49 | break 50 | except: 51 | self.stderr.write('Error: Version "%s" doesn\'t exist.\n' % version_name) 52 | version_name = None 53 | continue 54 | 55 | # filelisting 56 | filelisting = FileListing(path, filter_func=self.filter_images) # FIXME filterfunc: no hidden files, exclude list, no versions, just images! 57 | for fileobject in filelisting.files_walk_filtered(): 58 | if fileobject.filetype == "Image": 59 | if selected_version: 60 | self.stdout.write('generating version "%s" for: %s\n' % (selected_version, fileobject.path)) 61 | versionobject = fileobject.version_generate(selected_version) # FIXME force? 62 | else: 63 | self.stdout.write('generating all versions for: %s\n' % fileobject.path) 64 | for version in VERSIONS: 65 | versionobject = fileobject.version_generate(version) # FIXME force? 66 | 67 | # # walkt throu the filebrowser directory 68 | # # for all/new files (except file versions itself and excludes) 69 | # for dirpath,dirnames,filenames in os.walk(path, followlinks=True): 70 | # rel_dir = os.path.relpath(dirpath, os.path.realpath(settings.MEDIA_ROOT)) 71 | # for filename in filenames: 72 | # filtered = False 73 | # # no "hidden" files (stating with ".") 74 | # if filename.startswith('.'): 75 | # continue 76 | # # check the exclude list 77 | # for re_prefix in filter_re: 78 | # if re_prefix.search(filename): 79 | # filtered = True 80 | # if filtered: 81 | # continue 82 | # (tmp, extension) = os.path.splitext(filename) 83 | # if extension in EXTENSIONS["Image"]: 84 | # self.createVersions(os.path.join(rel_dir, filename), selected_version) 85 | 86 | def filter_images(self, item): 87 | filtered = item.filename.startswith('.') 88 | for re_prefix in filter_re: 89 | if re_prefix.search(item.filename): 90 | filtered = True 91 | if filtered: 92 | return False 93 | return True 94 | -------------------------------------------------------------------------------- /filebrowser/management/commands/fb_version_remove.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import re 4 | import sys 5 | 6 | from django.conf import settings 7 | from django.core.management.base import BaseCommand, CommandError 8 | from six.moves import input 9 | 10 | from filebrowser.settings import EXCLUDE, EXTENSIONS 11 | 12 | 13 | class Command(BaseCommand): 14 | args = '' 15 | help = "Remove Image-Versions within FILEBROWSER_DIRECTORY/MEDIA_ROOT." 16 | 17 | def handle(self, *args, **options): 18 | 19 | media_path = "" 20 | 21 | if len(args): 22 | media_path = args[0] 23 | 24 | path = os.path.join(settings.MEDIA_ROOT, media_path) 25 | 26 | if not os.path.isdir(path): 27 | raise CommandError(' must be a directory in MEDIA_ROOT. "%s" is no directory.' % path) 28 | 29 | self.stdout.write("\n%s\n" % self.help) 30 | self.stdout.write("in this case: %s\n" % path) 31 | 32 | # get suffix or prefix 33 | default_prefix_or_suffix = "s" 34 | while 1: 35 | self.stdout.write('\nOlder versions of the FileBrowser used to prefix the filename with the version name.\n') 36 | self.stdout.write('Current version of the FileBrowser adds the version name as suffix.\n') 37 | prefix_or_suffix = input('"p" for prefix or "s" for suffix (leave blank for "%s"): ' % default_prefix_or_suffix) 38 | 39 | if default_prefix_or_suffix and prefix_or_suffix == '': 40 | prefix_or_suffix = default_prefix_or_suffix 41 | if prefix_or_suffix != "s" and prefix_or_suffix != "p": 42 | sys.stderr.write('Error: "p" and "s" are the only valid inputs.\n') 43 | prefix_or_suffix = None 44 | continue 45 | break 46 | 47 | # get version name 48 | while 1: 49 | version_name = input('\nversion name as defined with VERSIONS: ') 50 | 51 | if version_name == "": 52 | self.stderr.write('Error: You have to enter a version name.\n') 53 | version_name = None 54 | continue 55 | else: 56 | break 57 | 58 | # get list of all matching files 59 | files = self.get_files(path, version_name, (prefix_or_suffix == "p")) 60 | 61 | # output (short version) of files to be deleted 62 | if len(files) > 15: 63 | self.stdout.write('\nFirst/Last 5 files to remove:\n') 64 | for current_file in files[:5]: 65 | self.stdout.write('%s\n' % current_file) 66 | self.stdout.write('...\n') 67 | self.stdout.write('...\n') 68 | for current_file in files[len(files) - 5:]: 69 | self.stdout.write('%s\n' % current_file) 70 | else: 71 | self.stdout.write('\nFiles to remove:\n') 72 | for current_file in files: 73 | self.stdout.write('%s\n' % current_file) 74 | 75 | # no files...done 76 | if len(files) == 0: 77 | self.stdout.write('0 files removed.\n\n') 78 | return 79 | else: 80 | self.stdout.write('%d file(s) will be removed.\n\n' % len(files)) 81 | 82 | # ask to make sure 83 | do_remove = "" 84 | self.stdout.write('Are Sure you want to delete these files?\n') 85 | do_remove = input('"y" for Yes or "n" for No (leave blank for "n"): ') 86 | 87 | # if "yes" we delete. any different case we finish without removing anything 88 | if do_remove == "y": 89 | for current_file in files: 90 | os.remove(current_file) 91 | self.stdout.write('%d file(s) removed.\n\n' % len(files)) 92 | else: 93 | self.stdout.write('No files removed.\n\n') 94 | return 95 | 96 | # get files mathing: 97 | # path: search recoursive in this path (os.walk) 98 | # version_name: string is pre/suffix of filename 99 | # search_for_prefix: if true we match against the start of the filename (default is the end) 100 | def get_files(self, path, version_name, search_for_prefix): 101 | file_list = [] 102 | # Precompile regular expressions 103 | filter_re = [] 104 | for exp in EXCLUDE: 105 | filter_re.append(re.compile(exp)) 106 | 107 | # walkt throu the filebrowser directory 108 | # for all/new files (except file versions itself and excludes) 109 | for dirpath, dirnames, filenames in os.walk(path, followlinks=True): 110 | for filename in filenames: 111 | filtered = False 112 | # no "hidden" files (stating with ".") 113 | if filename.startswith('.'): 114 | continue 115 | # check the exclude list 116 | for re_prefix in filter_re: 117 | if re_prefix.search(filename): 118 | filtered = True 119 | if filtered: 120 | continue 121 | (filename_noext, extension) = os.path.splitext(filename) 122 | # images only 123 | if extension in EXTENSIONS["Image"]: 124 | # if image matches with version_name we add it to the file_list 125 | if search_for_prefix: 126 | if filename_noext.startswith(version_name + "_"): 127 | file_list.append(os.path.join(dirpath, filename)) 128 | elif filename_noext.endswith("_" + version_name): 129 | file_list.append(os.path.join(dirpath, filename)) 130 | 131 | return file_list 132 | -------------------------------------------------------------------------------- /filebrowser/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class FileBrowser(models.Model): 6 | class Meta: 7 | managed = False 8 | verbose_name = _("FileBrowser") 9 | verbose_name_plural = _("FileBrowser") 10 | -------------------------------------------------------------------------------- /filebrowser/namers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import re 3 | 4 | import six 5 | from django.utils.encoding import force_str 6 | from django.utils.module_loading import import_string 7 | 8 | from .settings import VERSIONS, VERSION_NAMER 9 | 10 | 11 | def get_namer(**kwargs): 12 | namer_cls = import_string(VERSION_NAMER) 13 | return namer_cls(**kwargs) 14 | 15 | 16 | class VersionNamer(object): 17 | "Base namer only for reference" 18 | 19 | def __init__(self, **kwargs): 20 | self.kwargs = kwargs 21 | for k, v in kwargs.items(): 22 | setattr(self, k, v) 23 | 24 | def get_version_name(self): 25 | return self.file_object.filename_root + "_" + self.version_suffix + self.extension 26 | 27 | def get_original_name(self): 28 | tmp = self.file_object.filename_root.split("_") 29 | if tmp[len(tmp) - 1] in VERSIONS: 30 | return "%s%s" % ( 31 | self.file_object.filename_root.replace("_%s" % tmp[len(tmp) - 1], ""), 32 | self.file_object.extension) 33 | 34 | 35 | class OptionsNamer(VersionNamer): 36 | 37 | def get_version_name(self): 38 | name = "{root}_{options}{extension}".format( 39 | root=force_str(self.file_object.filename_root), 40 | options=self.options_as_string, 41 | extension=self.file_object.extension, 42 | ) 43 | return name 44 | 45 | def get_original_name(self): 46 | """ 47 | Restores the original file name wipping out the last 48 | `_version_suffix--plus-any-configs` block entirely. 49 | """ 50 | root = self.file_object.filename_root 51 | tmp = root.split("_") 52 | options_part = tmp[len(tmp) - 1] 53 | name = re.sub('_%s$' % options_part, '', root) 54 | return "%s%s" % (name, self.file_object.extension) 55 | 56 | @property 57 | def options_as_string(self): 58 | """ 59 | The options part should not contain `_` (underscore) on order to get 60 | original name back. 61 | """ 62 | name = '--'.join(self.options_list).replace(',', 'x') 63 | name = re.sub(r'[_\s]', '-', name) 64 | return re.sub(r'[^\w-]', '', name).strip() 65 | 66 | @property 67 | def options_list(self): 68 | opts = [] 69 | if not self.options: 70 | return opts 71 | 72 | if 'version_suffix' in self.kwargs: 73 | opts.append(self.kwargs['version_suffix']) 74 | 75 | if 'size' in self.options: 76 | opts.append('%sx%s' % tuple(self.options['size'])) 77 | elif 'width' in self.options or 'height' in self.options: 78 | width = float(self.options.get('width') or 0) 79 | height = float(self.options.get('height') or 0) 80 | opts.append('%dx%d' % (width, height)) 81 | 82 | for k, v in sorted(self.options.items()): 83 | if not v or k in ('size', 'width', 'height', 84 | 'quality', 'subsampling', 'verbose_name'): 85 | continue 86 | if v is True: 87 | opts.append(k) 88 | continue 89 | if not isinstance(v, six.string_types): 90 | try: 91 | v = 'x'.join([six.text_type(v) for item in v]) 92 | except TypeError: 93 | v = six.text_type(v) 94 | opts.append('%s-%s' % (k, v)) 95 | 96 | return opts 97 | -------------------------------------------------------------------------------- /filebrowser/settings.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.conf import settings 4 | from django.utils.translation import gettext_lazy as _ 5 | import re 6 | 7 | 8 | # Main FileBrowser Directory. Relative to site.storage.location. 9 | # DO NOT USE A SLASH AT THE BEGINNING, DO NOT FORGET THE TRAILING SLASH AT THE END. 10 | DIRECTORY = getattr(settings, "FILEBROWSER_DIRECTORY", 'uploads/') 11 | 12 | # EXTENSIONS AND FORMATS 13 | # Allowed Extensions for File Upload. Lower case is important. 14 | EXTENSIONS = getattr(settings, "FILEBROWSER_EXTENSIONS", { 15 | 'Image': ['.jpg', '.jpeg', '.gif', '.png', '.tif', '.tiff'], 16 | 'Document': ['.pdf', '.doc', '.rtf', '.txt', '.xls', '.csv', '.docx'], 17 | 'Video': ['.mov', '.mp4', '.m4v', '.webm', '.wmv', '.mpeg', '.mpg', '.avi', '.rm'], 18 | 'Audio': ['.mp3', '.wav', '.aiff', '.midi', '.m4p'] 19 | }) 20 | # Define different formats for allowed selections. 21 | # This has to be a subset of EXTENSIONS. 22 | # e.g., add ?type=image to the browse-URL ... 23 | SELECT_FORMATS = getattr(settings, "FILEBROWSER_SELECT_FORMATS", { 24 | 'file': ['Image', 'Document', 'Video', 'Audio'], 25 | 'image': ['Image'], 26 | 'document': ['Document'], 27 | 'media': ['Video', 'Audio'], 28 | }) 29 | 30 | # VERSIONS 31 | 32 | # Directory to Save Image Versions (and Thumbnails). Relative to site.storage.location. 33 | # If no directory is given, versions are stored within the Image directory. 34 | # VERSION URL: VERSIONS_BASEDIR/original_path/originalfilename_versionsuffix.extension 35 | VERSIONS_BASEDIR = getattr(settings, 'FILEBROWSER_VERSIONS_BASEDIR', '_versions') 36 | # Versions Format. Available Attributes: verbose_name, width, height, opts 37 | VERSIONS = getattr(settings, "FILEBROWSER_VERSIONS", { 38 | 'admin_thumbnail': {'verbose_name': 'Admin Thumbnail', 'width': 60, 'height': 60, 'opts': 'crop'}, 39 | 'thumbnail': {'verbose_name': 'Thumbnail (1 col)', 'width': 60, 'height': 60, 'opts': 'crop'}, 40 | 'small': {'verbose_name': 'Small (2 col)', 'width': 140, 'height': '', 'opts': ''}, 41 | 'medium': {'verbose_name': 'Medium (4col )', 'width': 300, 'height': '', 'opts': ''}, 42 | 'big': {'verbose_name': 'Big (6 col)', 'width': 460, 'height': '', 'opts': ''}, 43 | 'large': {'verbose_name': 'Large (8 col)', 'width': 680, 'height': '', 'opts': ''}, 44 | }) 45 | # Quality of saved versions 46 | VERSION_QUALITY = getattr(settings, 'FILEBROWSER_VERSION_QUALITY', 90) 47 | # Versions available within the Admin-Interface. 48 | ADMIN_VERSIONS = getattr(settings, 'FILEBROWSER_ADMIN_VERSIONS', ['thumbnail', 'small', 'medium', 'big', 'large']) 49 | # Which Version should be used as Admin-thumbnail. 50 | ADMIN_THUMBNAIL = getattr(settings, 'FILEBROWSER_ADMIN_THUMBNAIL', 'admin_thumbnail') 51 | 52 | VERSION_PROCESSORS = getattr(settings, 'FILEBROWSER_VERSION_PROCESSORS', [ 53 | 'filebrowser.utils.scale_and_crop', 54 | ]) 55 | VERSION_NAMER = getattr(settings, 'FILEBROWSER_VERSION_NAMER', 'filebrowser.namers.VersionNamer') 56 | 57 | # PLACEHOLDER 58 | 59 | # Path to placeholder image (relative to storage location) 60 | PLACEHOLDER = getattr(settings, "FILEBROWSER_PLACEHOLDER", "") 61 | # Show Placeholder if the original image does not exist 62 | SHOW_PLACEHOLDER = getattr(settings, "FILEBROWSER_SHOW_PLACEHOLDER", False) 63 | # Always show placeholder (even if the original image exists) 64 | FORCE_PLACEHOLDER = getattr(settings, "FILEBROWSER_FORCE_PLACEHOLDER", False) 65 | 66 | # EXTRA SETTINGS 67 | 68 | # If set to True, the FileBrowser will not try to import a mis-installed PIL. 69 | STRICT_PIL = getattr(settings, 'FILEBROWSER_STRICT_PIL', False) 70 | # PIL's Error "Suspension not allowed here" work around: 71 | # s. http://mail.python.org/pipermail/image-sig/1999-August/000816.html 72 | IMAGE_MAXBLOCK = getattr(settings, 'FILEBROWSER_IMAGE_MAXBLOCK', 1024 * 1024) 73 | # Exclude files matching any of the following regular expressions 74 | # Default is to exclude 'thumbnail' style naming of image-thumbnails. 75 | EXTENSION_LIST = [] 76 | for exts in EXTENSIONS.values(): 77 | EXTENSION_LIST += [re.escape(ext) for ext in exts if ext] 78 | EXCLUDE = getattr(settings, 'FILEBROWSER_EXCLUDE', (r'_(%(exts)s)_.*_q\d{1,3}\.(%(exts)s)' % {'exts': ('|'.join(EXTENSION_LIST))},)) 79 | # Max. Upload Size in Bytes. 80 | MAX_UPLOAD_SIZE = getattr(settings, "FILEBROWSER_MAX_UPLOAD_SIZE", 10485760) 81 | # Normalize filename and remove all non-alphanumeric characters 82 | # except for underscores, spaces & dashes. 83 | NORMALIZE_FILENAME = getattr(settings, "FILEBROWSER_NORMALIZE_FILENAME", False) 84 | # Convert Filename (replace spaces and convert to lowercase) 85 | CONVERT_FILENAME = getattr(settings, "FILEBROWSER_CONVERT_FILENAME", True) 86 | # Max. Entries per Page 87 | # Loading a Sever-Directory with lots of files might take a while 88 | # Use this setting to limit the items shown 89 | LIST_PER_PAGE = getattr(settings, "FILEBROWSER_LIST_PER_PAGE", 50) 90 | # Default Sorting 91 | # Options: date, filesize, filename_lower, filetype_checked 92 | DEFAULT_SORTING_BY = getattr(settings, "FILEBROWSER_DEFAULT_SORTING_BY", "date") 93 | # Sorting Order: asc, desc 94 | DEFAULT_SORTING_ORDER = getattr(settings, "FILEBROWSER_DEFAULT_SORTING_ORDER", "desc") 95 | # regex to clean dir names before creation 96 | FOLDER_REGEX = getattr(settings, "FILEBROWSER_FOLDER_REGEX", r'^[\w._\ /-]+$') 97 | # Traverse directories when searching 98 | SEARCH_TRAVERSE = getattr(settings, "FILEBROWSER_SEARCH_TRAVERSE", False) 99 | # Default Upload and Version Permissions 100 | DEFAULT_PERMISSIONS = getattr(settings, "FILEBROWSER_DEFAULT_PERMISSIONS", 0o755) 101 | # Overwrite existing files on upload 102 | OVERWRITE_EXISTING = getattr(settings, "FILEBROWSER_OVERWRITE_EXISTING", True) 103 | # Add fake model to show filebrowser in admin dashboard 104 | SHOW_IN_DASHBOARD = getattr(settings, "FILEBROWSER_SHOW_IN_DASHBOARD", True) 105 | 106 | # UPLOAD 107 | 108 | # Directory to Save temporary uploaded files (FileBrowseUploadField) 109 | # Relative to site.storage.location. 110 | UPLOAD_TEMPDIR = getattr(settings, 'FILEBROWSER_UPLOAD_TEMPDIR', '_temp') 111 | 112 | # EXTRA TRANSLATION STRINGS 113 | 114 | # The following strings are not available within views or templates 115 | _('Folder') 116 | _('Image') 117 | _('Video') 118 | _('Document') 119 | _('Audio') 120 | 121 | 122 | # Overwrite admin_site. 123 | # Example: 124 | # in file: apps.core.admin define this code. 125 | 126 | # from django.contrib import admin 127 | # class CustomAdmin(admin.AdminSite): 128 | # site_header = site_title = 'Custom Admin' 129 | # index_title = 'Custom Administration' 130 | 131 | # admin_general = CustomAdmin(name='admin_general') 132 | 133 | # In settings: 134 | # FILEBROWSER_CUSTOM_ADMIN = 'apps.core.admin.admin_general' 135 | 136 | ADMIN_CUSTOM = getattr(settings, 'FILEBROWSER_CUSTOM_ADMIN', None) 137 | -------------------------------------------------------------------------------- /filebrowser/signals.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.dispatch import Signal 4 | 5 | 6 | # upload signals 7 | # path: Absolute server path to the file/folder 8 | # name: Name of the file/folder 9 | # site: Current FileBrowserSite instance 10 | filebrowser_pre_upload = Signal() # providing_args=["path", "file", "site"]) 11 | filebrowser_post_upload = Signal() # providing_args=["path", "file", "site"]) 12 | 13 | # mkdir signals 14 | # path: Absolute server path to the file/folder 15 | # name: Name of the file/folder 16 | # site: Current FileBrowserSite instance 17 | filebrowser_pre_createdir = Signal() # providing_args=["path", "name", "site"]) 18 | filebrowser_post_createdir = Signal() # providing_args=["path", "name", "site"]) 19 | 20 | # delete signals 21 | # path: Absolute server path to the file/folder 22 | # name: Name of the file/folder 23 | # site: Current FileBrowserSite instance 24 | filebrowser_pre_delete = Signal() # providing_args=["path", "name", "site"]) 25 | filebrowser_post_delete = Signal() # providing_args=["path", "name", "site"]) 26 | 27 | # rename signals 28 | # path: Absolute server path to the file/folder 29 | # name: Name of the file/folder 30 | # site: Current FileBrowserSite instance 31 | # new_name: New name of the file/folder 32 | filebrowser_pre_rename = Signal() # providing_args=["path", "name", "new_name", "site"]) 33 | filebrowser_post_rename = Signal() # providing_args=["path", "name", "new_name", "site"]) 34 | 35 | # action signals 36 | # action_name: Name of the custom action 37 | # fileobjects: A list of fileobjects the action will be applied to 38 | # site: Current FileBrowserSite instance 39 | # result: The response you defined with your custom action 40 | filebrowser_actions_pre_apply = Signal() # providing_args=['action_name', 'fileobjects', 'site']) 41 | filebrowser_actions_post_apply = Signal() # providing_args=['action_name', 'filebjects', 'result', 'site']) 42 | -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/css/filebrowser.css: -------------------------------------------------------------------------------- 1 | /* Filebrowser Pulldown Actions 2 | ------------------------------------------------------------------------------------------------------ */ 3 | 4 | .fb_icon { 5 | white-space: nowrap; 6 | } 7 | .pulldown-versions-container { 8 | position: relative; 9 | display: inline-block; 10 | } 11 | .pulldown-versions-handler { 12 | position: relative; 13 | display: inline-block; 14 | width: 14px; 15 | margin-right: 2px !important; 16 | } 17 | .pulldown-versions-handler:after { 18 | position: absolute; 19 | display: block; 20 | content: ''; 21 | left: 50%; 22 | top: 50%; 23 | width: 0px; 24 | height: 0px; 25 | border-left: 6px solid transparent; 26 | border-right: 6px solid transparent; 27 | border-top: 6px solid #fff; 28 | margin-left: -6px; 29 | margin-top: -3px; 30 | } 31 | .pulldown-versions-handler:focus { 32 | text-decoration: none; 33 | } 34 | .open .pulldown-versions-handler:after { 35 | border-top: 0; 36 | border-bottom: 6px solid #fff; 37 | } 38 | ul.pulldown-versions { 39 | display: none; 40 | margin: 0; 41 | padding: 0; 42 | background: #f8f8f8; 43 | border: 1px solid #ccc; 44 | } 45 | ul.pulldown-versions.open { 46 | display: block; 47 | position: absolute; 48 | z-index: 1000; 49 | } 50 | ul.pulldown-versions li { 51 | list-style: none; 52 | } 53 | ul.pulldown-versions a { 54 | display: block; 55 | padding: 5px 15px; 56 | white-space: nowrap; 57 | } 58 | 59 | /* Table Header */ 60 | thead th.sorted .text { 61 | padding-right: 24px; 62 | } 63 | 64 | /* Upload button 65 | ------------------------------------------------------------------------------------------------------ */ 66 | .qq-upload-button { 67 | background: #309bbf; /* Old browsers */ 68 | background: -moz-linear-gradient(top, #36b0d9 0%, #309bbf 100%); /* FF3.6+ */ 69 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#36b0d9), color-stop(100%,#309bbf)); /* Chrome,Safari4+ */ 70 | background: -webkit-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* Chrome10+,Safari5.1+ */ 71 | background: -o-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* Opera11.10+ */ 72 | background: -ms-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* IE10+ */ 73 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#36b0d9', endColorstr='#309bbf',GradientType=0 ); /* IE6-9 */ 74 | background: linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* W3C */ 75 | 76 | border: 1px solid #2987a6; 77 | -webkit-border-radius: 3px; 78 | -moz-border-radius: 3px; 79 | border-radius: 3px; 80 | 81 | padding: 6px; 82 | font: normal 13px/1 Arial, sans-serif; 83 | color: white; 84 | text-transform: uppercase; 85 | text-align: center; 86 | width: 140px; 87 | } 88 | .qq-upload-button:hover { 89 | background: #333333; /* Old browsers */ 90 | background: -moz-linear-gradient(top, #444444 0%, #333333 100%); /* FF3.6+ */ 91 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#444444), color-stop(100%,#333333)); /* Chrome,Safari4+ */ 92 | background: -webkit-linear-gradient(top, #444444 0%,#333333 100%); /* Chrome10+,Safari5.1+ */ 93 | background: -o-linear-gradient(top, #444444 0%,#333333 100%); /* Opera11.10+ */ 94 | background: -ms-linear-gradient(top, #444444 0%,#333333 100%); /* IE10+ */ 95 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#444444', endColorstr='#333333',GradientType=0 ); /* IE6-9 */ 96 | background: linear-gradient(top, #444444 0%,#333333 100%); /* W3C */ 97 | 98 | border: 1px solid #333; 99 | } 100 | .qq-upload-list { 101 | position: relative; 102 | clear: both; 103 | margin: 8px -12px 0 -12px; 104 | border-top: 1px solid #e0e0e0; 105 | } 106 | .qq-upload-list:empty { 107 | margin-top: 0; 108 | border-top: 0; 109 | } 110 | .qq-upload-list > div { 111 | position: relative; 112 | font: 12px Arial, sans-serif; 113 | padding: 8px 12px; 114 | border-top: 1px solid #fff; 115 | border-bottom: 1px solid #e0e0e0; 116 | } 117 | .qq-upload-list > div + > div { 118 | border-top: 1px solid #fff; 119 | } 120 | .qq-upload-list > div:last-child { 121 | padding-bottom: 0; 122 | border-bottom: 0; 123 | } 124 | span.qq-upload-file { 125 | font-weight: bold; 126 | } 127 | .qq-upload-failed-text { 128 | display: none; 129 | } 130 | .qq-upload-fail .qq-upload-failed-text { 131 | display: inline; 132 | margin-left: 10px; 133 | color: #bf3030; 134 | font-weight: bold; 135 | } 136 | .qq-upload-cancel { 137 | position: absolute; 138 | right: 14px; 139 | margin: 0; 140 | color: transparent !important; 141 | width: 16px; 142 | height: 16px; 143 | background: url('../img/cancel.png') top left no-repeat; 144 | cursor: pointer; 145 | } 146 | .qq-upload-cancel:hover { 147 | color: transparent !important; 148 | background: url('../img/cancel_hover.png') top left no-repeat; 149 | } 150 | .qq-upload-size { 151 | font-size: 11px; 152 | margin-left: 10px; 153 | } 154 | .qq-upload-complete { 155 | display: none; 156 | position: absolute; 157 | float: right; 158 | left: 334px; 159 | margin: -15px -2px 0 0; 160 | color: transparent !important; 161 | width: 16px; 162 | height: 16px; 163 | background: url('../img/completed.png') top left no-repeat; 164 | } 165 | .qq-upload-success .qq-upload-complete { 166 | display: block; 167 | } 168 | .progress-bar { 169 | -webkit-border-radius: 2px; 170 | -moz-border-radius: 2px; 171 | border-radius: 2px; 172 | 173 | background-color: #fff; 174 | border: 1px solid #999; 175 | height: 6px; 176 | margin: 4px 0 2px; 177 | } 178 | .progress-bar .content { 179 | background: #309bbf; /* Old browsers */ 180 | background: -moz-linear-gradient(top, #36b0d9 0%, #309bbf 100%); /* FF3.6+ */ 181 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#36b0d9), color-stop(100%,#309bbf)); /* Chrome,Safari4+ */ 182 | background: -webkit-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* Chrome10+,Safari5.1+ */ 183 | background: -o-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* Opera11.10+ */ 184 | background: -ms-linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* IE10+ */ 185 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#444444', endColorstr='#309bbf',GradientType=0 ); /* IE6-9 */ 186 | background: linear-gradient(top, #36b0d9 0%,#309bbf 100%); /* W3C */ 187 | 188 | height: 6px; 189 | width: 0%; 190 | } 191 | .qq-upload-success .progress-bar .content { 192 | background: #333333; /* Old browsers */ 193 | background: -moz-linear-gradient(top, #444444 0%, #333333 100%); /* FF3.6+ */ 194 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#444444), color-stop(100%,#333333)); /* Chrome,Safari4+ */ 195 | background: -webkit-linear-gradient(top, #444444 0%,#333333 100%); /* Chrome10+,Safari5.1+ */ 196 | background: -o-linear-gradient(top, #444444 0%,#333333 100%); /* Opera11.10+ */ 197 | background: -ms-linear-gradient(top, #444444 0%,#333333 100%); /* IE10+ */ 198 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#444444', endColorstr='#333333',GradientType=0 ); /* IE6-9 */ 199 | background: linear-gradient(top, #444444 0%,#333333 100%); /* W3C */ 200 | } 201 | .qq-upload-success .progress-bar .content:after { 202 | background: transparent url('../img/cancel.png') 0 0 no-repeat scroll; 203 | } 204 | 205 | /* Popup fixes */ 206 | .popup.filebrowser .breadcrumbs { 207 | padding-left: 10px; 208 | } 209 | 210 | .popup.filebrowser .object-tools { 211 | margin-top: -35px; 212 | margin-right: 10px; 213 | } 214 | 215 | .popup.filebrowser h1 { 216 | display: none; 217 | } 218 | 219 | .popup.filebrowser.change-list .filtered .results, 220 | .popup.filebrowser.change-list .filtered #toolbar { 221 | margin-right: 260px; 222 | } 223 | -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/css/uploadfield.css: -------------------------------------------------------------------------------- 1 | .fb-uploader-container { 2 | position: relative; 3 | display: inline-block; 4 | width: 38px; 5 | height: 19px; 6 | vertical-align: middle; 7 | } 8 | .fb-uploader-container .fb-uploader { 9 | display: inline-block; 10 | height: 17px 11 | } 12 | .fb-uploader-container .fb-upload-drop-area { 13 | display: none !important 14 | } 15 | .fb-uploader-container .fb-upload-button { 16 | position: relative; 17 | float: right; 18 | display: inline-block; 19 | width: 17px; 20 | height: 17px; 21 | font-size: 0; 22 | line-height: 0; 23 | border: 1px solid #ccc; 24 | background-position: 0 0; 25 | background-repeat: no-repeat; 26 | background-attachment: scroll; 27 | background-image: url("../img/fb-upload.png"), url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ViZjJmNSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2UxZjBmNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 28 | background-size: 100%; 29 | background-image: url("../img/fb-upload.png"), -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ebf2f5), color-stop(100%, #e1f0f5)); 30 | background-image: url("../img/fb-upload.png"), -webkit-linear-gradient(#ebf2f5, #e1f0f5); 31 | background-image: url("../img/fb-upload.png"), -moz-linear-gradient(#ebf2f5, #e1f0f5); 32 | background-image: url("../img/fb-upload.png"), -o-linear-gradient(#ebf2f5, #e1f0f5); 33 | background-image: url("../img/fb-upload.png"), linear-gradient(#ebf2f5, #e1f0f5) 34 | } 35 | .fb-uploader-container .fb-upload-button:hover { 36 | background-image: url("../img/fb-upload_hover.png"), url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2UxZTFlMSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VlZWVlZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 37 | background-size: 100%; 38 | background-image: url("../img/fb-upload_hover.png"), -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e1e1e1), color-stop(100%, #eeeeee)); 39 | background-image: url("../img/fb-upload_hover.png"), -webkit-linear-gradient(#e1e1e1, #eeeeee); 40 | background-image: url("../img/fb-upload_hover.png"), -moz-linear-gradient(#e1e1e1, #eeeeee); 41 | background-image: url("../img/fb-upload_hover.png"), -o-linear-gradient(#e1e1e1, #eeeeee); 42 | background-image: url("../img/fb-upload_hover.png"), linear-gradient(#e1e1e1, #eeeeee) 43 | } 44 | .fb-uploader-container .fb-upload-list { 45 | position: relative; 46 | float: left; 47 | width: 19px; 48 | height: 19px; 49 | background: transparent 50 | } 51 | .fb-uploader-container .fb-upload-list .fb-upload-item { 52 | position: relative 53 | } 54 | .fb-uploader-container .fb-upload-list .fb-upload-item span, 55 | .fb-uploader-container .fb-upload-list .fb-upload-item a { 56 | position: absolute; 57 | display: inline-block; 58 | width: 19px; 59 | height: 19px 60 | } 61 | .fb-uploader-container .fb-upload-list .fb-upload-item .fb-upload-file, 62 | .fb-uploader-container .fb-upload-list .fb-upload-item .fb-upload-size { 63 | display: none !important 64 | } 65 | .fb-uploader-container .fb-upload-list .fb-upload-item .fb-upload-spinner { 66 | background: transparent url("../img/fb-upload-spinner.gif") 50% 50% no-repeat 67 | } 68 | .fb-uploader-container .fb-upload-list .fb-upload-item .progress-bar { 69 | position: absolute; 70 | z-index: 1000; 71 | padding: 1px; 72 | margin-left: -8px; /* input borders + paddings */ 73 | height: 17px; 74 | background: transparent 75 | } 76 | .fb-uploader-container .fb-upload-list .fb-upload-item .progress-bar .content { 77 | width: 0; 78 | height: 17px; 79 | background: #fff url("../img/progress-bar-content.png") 0 0 repeat-x scroll 80 | } 81 | .fb-uploader-container+a.fb_show { 82 | margin-bottom: 0 !important 83 | } 84 | .fb-uploadfield { 85 | margin: 0; 86 | padding: 0; 87 | min-height: 25px 88 | } 89 | .fb-uploadfield.error input.vFileBrowseUploadField, 90 | .fb-uploadfield.error a.fb_show, 91 | .fb-uploadfield.error .fb-upload-button, 92 | .fb-uploadfield.error .fb-upload-spinner { 93 | border-color: #BF3030 !important 94 | } 95 | .fb-uploadfield ul.errorlist { 96 | margin-top: 5px !important 97 | } 98 | .fb-uploadfield ul.errorlist+p.preview { 99 | margin-top: 2px !important 100 | } 101 | .fb-uploadfield .fb-uploadfield ul.errorlist+p.preview { 102 | margin-top: 2px !important 103 | } 104 | .fb-uploadfield .progress-bar { 105 | left: -20em; 106 | width: 20em; 107 | } 108 | -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/TEST_IMAGE_000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/TEST_IMAGE_000.jpg -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/cancel.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/cancel_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/cancel_hover.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/completed.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/fb-upload-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/fb-upload-spinner.gif -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/fb-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/fb-upload.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/fb-upload_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/fb-upload_hover.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/filebrowser_icon_show.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/filebrowser_icon_show.gif -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/filebrowser_icon_show_hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/filebrowser_icon_show_hover.gif -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/icon-pulldown-versions-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/icon-pulldown-versions-active.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/icon-pulldown-versions-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/icon-pulldown-versions-hover.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/icon-pulldown-versions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/icon-pulldown-versions.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/progress-bar-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/progress-bar-content.png -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/test_not_an_image.jpg: -------------------------------------------------------------------------------- 1 | not an image 2 | -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/img/testimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smacker/django-filebrowser-no-grappelli/HEAD/filebrowser/static/filebrowser/img/testimage.jpg -------------------------------------------------------------------------------- /filebrowser/static/filebrowser/js/AddFileBrowser.js: -------------------------------------------------------------------------------- 1 | var FileBrowser = { 2 | // this is set automatically 3 | admin_media_prefix: '', 4 | // change this 5 | thumb_prefix: 'thumb_', 6 | no_thumb: 'filebrowser/img/no_thumb.gif', 7 | 8 | init: function() { 9 | // Deduce admin_media_prefix by looking at the 109 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | 4 | {% load static i18n fb_tags fb_versions fb_compat %} 5 | 6 | 7 | {% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} 8 | 9 | 10 | {% block content %} 11 | 12 | {% if query.pop %} 13 | {% include "filebrowser/include/breadcrumbs.html" %} 14 | {% endif %} 15 |

{% blocktrans with fileobject.filename as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

16 | 17 |
    18 |
  • {{ fileobject.filename }}
  • 19 |
20 | 21 | {% if filelisting %} 22 |
    23 | {% for item in filelisting %} 24 |
  • {{ item.path_relative_directory }}
  • 25 | {% endfor %} 26 |
      27 | {% endif %} 28 | {% if additional_files %} 29 |
        30 |
      • {% blocktrans with additional_files as escaped_object %}... and {{ escaped_object }} more Files.{% endblocktrans %}
      • 31 |
      32 | {% endif %} 33 | 34 |
      35 | {% csrf_token %} 36 |
      37 | 38 |
      39 |
      40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | 4 | {% load static i18n fb_tags fb_versions fb_compat %} 5 | 6 | 7 | {% block extrastyle %} 8 | {{ block.super }} 9 | 10 | {% endblock %} 11 | 12 | 13 | {% block extrahead %} 14 | {{ block.super }} 15 | 16 | 17 | 27 | {% endblock %} 28 | 29 | 30 | {% block bodyclass %}change-form filebrowser{% if query.pop %} popup{% endif %}{% endblock %} 31 | {% block coltype %}colM{% endblock %} 32 | 33 | 34 | {% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} 35 | 36 | 37 | {% block content %} 38 |
      39 | 40 | {% if query.pop %} 41 | {% include "filebrowser/include/breadcrumbs.html" %} 42 | {% endif %} 43 |
      {% csrf_token %} 44 |
      45 | {% if form.errors %}

      {% trans 'Please correct the following errors.' %}

      {% endif %} 46 |
      47 |
      48 | {% if form.name.errors %}{{ form.name.errors }}{% endif %} 49 |
      50 | 51 | {{ form.name }} 52 | {% if form.name.help_text %}

      {{ form.name.help_text|safe }}

      {% endif %} 53 |
      54 |
      55 |
      56 |
      57 |

      {% trans "Edit" %}

      58 |
      59 | {% if form.custom_action.errors %}{{ form.custom_action.errors }}{% endif %} 60 |
      61 | 62 | {{ form.custom_action }} 63 | {% if form.custom_action.help_text %}

      {{ form.custom_action.help_text|safe }}

      {% endif %} 64 |
      65 |
      66 |
      67 | {% if fileobject.filetype == "Folder" %} 68 |
      69 |

      {% trans "Folder Information" %}

      70 |
      71 |
      72 | 73 |

      74 | {{ fileobject.datetime|date:"N j, Y" }} 75 |

      76 |
      77 |
      78 |
      79 | {% endif %} 80 | {% if fileobject.filetype != "Folder" %} 81 |
      82 |

      {% trans "File Information" %}

      83 |
      84 |
      85 | 86 |

      87 | {{ fileobject.url }} 88 |

      89 |
      90 |
      91 |
      92 |
      93 | 94 |

      95 | {{ fileobject.filesize|filesizeformat }} 96 |

      97 |
      98 |
      99 |
      100 |
      101 | 102 |

      103 | {{ fileobject.datetime|date:"N j, Y" }} 104 |

      105 |
      106 |
      107 | {% if fileobject.filetype == "Image" %} 108 |
      109 |
      110 | 111 |

      112 | {{ fileobject.width }} x {{ fileobject.height }} px 113 |

      114 |
      115 |
      116 | {% endif %} 117 |
      118 | {% endif %} 119 | {% if fileobject.filetype == "Image" %} 120 |
      121 |

      {% trans "Image Versions" %}

      122 | {% if settings_var.ADMIN_THUMBNAIL %} 123 |
      124 |
      125 | 126 | 127 |
      128 |
      129 | {% endif %} 130 | {% if settings_var.ADMIN_VERSIONS %} 131 | {% for version in settings_var.ADMIN_VERSIONS %} 132 | {% version fileobject version as image_version %} 133 | {% version_setting version %} 134 |
      135 |
      136 | 137 | 138 |
      139 |
      140 | {% endfor %} 141 | {% endif %} 142 |
      143 | {% endif %} 144 |
      145 | 148 | 149 | 150 |
      151 |
      152 |
      153 |
      154 | {% endblock %} 155 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/_response.html: -------------------------------------------------------------------------------- 1 | {{ response }} -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags fb_compat %} 2 | 3 | 30 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/filelisting.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags fb_versions fb_compat %} 2 | 3 | {% for fileobject in page.object_list %} 4 | 5 | 6 | {% if fileobject.filetype == "Image" %} 7 | {% version fileobject settings_var.ADMIN_THUMBNAIL as thumbnail_version %} 8 | {% endif %} 9 | 10 | 11 | 12 | 13 | {% if query.pop == "1" %} 14 | 15 | {% selectable fileobject.filetype query.type %} 16 | {% if selectable %} 17 | 18 | {% if fileobject.filetype == "Image" and settings_var.ADMIN_VERSIONS %} 19 |
      20 |   21 | 27 |
      28 | {% endif %} 29 | 30 | {% trans "Select" %} 31 | {% else %} 32 |   33 | {% endif %} 34 | 35 | {% endif %} 36 | 37 | 38 | 39 | {% if query.pop == "2" or query.pop == "4" or query.pop == "5" %} 40 | 41 | {% selectable fileobject.filetype query.type %} 42 | {% if selectable %} 43 | 44 | {% if fileobject.filetype == "Image" and settings_var.ADMIN_VERSIONS %} 45 |
      46 |   47 | 53 |
      54 | {% endif %} 55 | 56 | {% trans "Select" %} 57 | {% else %} 58 |   59 | {% endif %} 60 | 61 | {% endif %} 62 | 63 | 64 | {% if query.pop == "3" %} 65 | 66 | {% selectable fileobject.filetype query.type %} 67 | {% if selectable %} 68 | 69 | {% if fileobject.filetype == "Image" and settings_var.ADMIN_VERSIONS %} 70 |
      71 |   72 | 78 |
      79 | {% endif %} 80 | 81 | {% trans "Select" %} 82 | {% else %} 83 |   84 | {% endif %} 85 | 86 | {% endif %} 87 | 88 | 89 | 90 | {% if fileobject.filetype %} 91 | {% trans fileobject.filetype %} 92 | {% else %} 93 | 94 | {% endif %} 95 | 96 | 97 | 98 | 99 | {% if fileobject.filetype == "Image" %} 100 | 101 | {% endif %} 102 | 103 | 104 | 105 | {% if fileobject.is_folder %} 106 | {{ fileobject.filename }} 107 | {% else %} 108 | 109 | {{ fileobject.filename }} 110 | {% if fileobject.dimensions %} 111 |
      {{ fileobject.dimensions.0 }} x {{ fileobject.dimensions.1 }} px 112 | {% endif %} 113 | 114 | {% endif %} 115 | 116 | 117 | {% if query.q and settings_var.SEARCH_TRAVERSE %} 118 | {{ fileobject.folder }} 119 | {% endif %} 120 | 121 | 122 | {% if fileobject.filesize %}{{ fileobject.filesize|filesizeformat }}{% else %}—{% endif %} 123 | 124 | 125 | {{ fileobject.datetime|date:"N j, Y" }} 126 | 127 | 128 | 129 | {% trans "Change" %} 130 | 131 | 132 | 133 | {% endfor %} 134 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/filter.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags %} 2 |
      3 |

      {% trans 'Filter' %}

      4 | 5 |

      {% trans "By Date" %}

      6 | 18 | 19 |

      {% trans "By Type" %}

      20 | 30 |
      31 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags %} 2 | 3 |

      4 | {% if page_range %} 5 | {% for i in page_range %} 6 | {% if i == "." %} 7 | ... 8 | {% else %} 9 | {% if i == page_num %} 10 | {{ i|add:"1" }} 11 | {% else %} 12 | {{ i|add:"1" }} 13 | {% endif %} 14 | {% endif %} 15 | {% endfor %} 16 | {% endif %} 17 | {{ filelisting.results_total }} {% trans 'total' %} 18 | {% if query.q or filelisting.results_total != filelisting.results_current %}  {% trans 'Show all' %}{% endif %} 19 |

      20 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/tableheader.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags %} 2 | 3 | 4 | 5 | {% comment %}{% if not query.pop %}{% endif %}{% endcomment %} 6 | 7 | {% if query.pop == "1" %}{% endif %} 8 | {% if query.pop == "2" %}{% endif %} 9 | {% if query.pop == "3" %}{% endif %} 10 | {% if query.pop == "4" %}{% endif %} 11 | {% if query.pop == "5" %}{% endif %} 12 | 13 | {% if query.o == "filetype" %} 14 | 15 |
      16 | 17 |
      18 | 19 |
      20 | 21 | {% endif %} 22 | {% if query.o != "filetype" %} 23 | 24 | 25 |
      26 | 27 | {% endif %} 28 | 29 | 30 |
      {% trans "Thumbnail" %}
      31 |
      32 | 33 | 34 | {% if query.o == "filename_lower" %} 35 | 36 |
      37 | 38 |
      39 | 40 |
      41 | {% endif %} 42 | {% if query.o != "filename_lower" %} 43 | 44 | 45 |
      46 | {% endif %} 47 | 48 | {% if query.q and settings_var.SEARCH_TRAVERSE %} 49 | {% if query.o == 'folder' %} 50 | 51 |
      52 | 53 |
      54 | 55 |
      56 | 57 | {% endif %} 58 | {% if query.o != 'folder' %} 59 | 60 | 61 |
      62 | 63 | {% endif %} 64 | {% endif %} 65 | 66 | {% if query.o == "filesize" %} 67 | 68 |
      69 | 70 |
      71 | 72 |
      73 | 74 | {% endif %} 75 | {% if query.o != "filesize" %} 76 | 77 | 78 |
      79 | 80 | {% endif %} 81 | 82 | {% if query.o == "date" %} 83 | 84 |
      85 | 86 |
      87 | 88 |
      89 | 90 | {% endif %} 91 | {% if query.o != "date" %} 92 | 93 | 94 |
      95 | 96 | {% endif %} 97 | 98 | 99 | 100 |
       
      101 |
      102 | 103 | 104 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/include/toolbar.html: -------------------------------------------------------------------------------- 1 | {% load i18n fb_tags %} 2 | 3 | {% if results_var.results_total %} 4 | {% if query.filter_type or query.filter_date or query.q %} 5 |
      6 |

      {% trans 'Results' %}

      7 |
      8 |

      {% blocktrans count results_var.results_current as counter %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %}

      9 |

      {% blocktrans with results_var.results_total as full_result_count %}{{ full_result_count }} total{% endblocktrans %}

      10 |
      11 |
      12 | {% endif %} 13 | {% endif %} 14 | 15 | 36 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | 4 | {% load static i18n l10n fb_tags fb_compat %} 5 | 6 | 7 | {% block extrastyle %} 8 | {{ block.super }} 9 | 10 | 11 | {% endblock %} 12 | 13 | 14 | {% block extrahead %} 15 | {{ block.super }} 16 | 17 | 18 | 19 | 20 | 21 | 88 | {% endblock %} 89 | 90 | 91 | {% block bodyclass %}change-form filebrowser{% if query.pop %} popup{% endif %}{% endblock %} 92 | {% block coltype %}colM{% endblock %} 93 | 94 | 95 | {% block breadcrumbs %}{% include "filebrowser/include/breadcrumbs.html" %}{% endblock %} 96 | 97 | 98 | {% block content %} 99 |
      100 | 101 | {% if query.pop %} 102 | {% include "filebrowser/include/breadcrumbs.html" %} 103 | {% endif %} 104 |
      105 |
      106 |
      107 |
      108 |
      109 | 112 |
      113 |
      114 |
      115 | {% with breadcrumbs|last as last_breadcrumb %} 116 | {% trans 'Return to uploads' %} 117 | {% endwith %} 118 |
      119 |
      120 |
      121 |

      {% trans "Help" %}

      122 |
      123 |
      124 | 125 |

      126 | {% for extension in settings_var.EXTENSIONS.items %} 127 | {% if extension.0 != 'Folder' %} 128 | {% trans extension.0|safe %} ({{ extension.1|join:", "|safe }})
      129 | {% endif %} 130 | {% endfor %} 131 |

      132 |
      133 |
      134 |
      135 |
      136 | 137 |

      {{ settings_var.MAX_UPLOAD_SIZE|filesizeformat }}

      138 |
      139 |
      140 | {% if settings_var.NORMALIZE_FILENAME or settings_var.CONVERT_FILENAME %} 141 |
      142 |
      143 | 144 | {% if settings_var.NORMALIZE_FILENAME %} 145 |

      {% trans "The Name will be normalized by converting or stripping all non-standard characters." %}

      146 | {% endif %} 147 | {% if settings_var.CONVERT_FILENAME %} 148 |

      {% trans "The Name will be converted to lowercase. Spaces will be replaced with underscores." %}

      149 | {% endif %} 150 |
      151 |
      152 | {% endif %} 153 |
      154 |
      155 |
      156 |
      157 | {% endblock %} 158 | -------------------------------------------------------------------------------- /filebrowser/templates/filebrowser/version.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | 4 | {% load static i18n fb_tags fb_versions fb_compat %} 5 | 6 | 7 | {% block extrahead %} 8 | {{ block.super }} 9 | 10 | 11 | {% if query.pop == '1' %} 12 | 13 | {% endif %} 14 | {% if query.pop == '2' %} 15 | 16 | 17 | {% if query.mce_rdomain %}{% endif %} 18 | {% endif %} 19 | {% if query.pop == '4' %} 20 | 21 | {% endif %} 22 | {% if query.pop == '5' %} 23 | 24 | {% endif %} 25 | {% endblock %} 26 | 27 | 28 | {% block content %} 29 | {% if fileobject.filetype == "Image" %} 30 | {% version fileobject.path settings_var.ADMIN_THUMBNAIL as thumbnail_version %} 31 | {% version fileobject.path query.version as image_version %} 32 | {% if query.pop == '1' %} 33 | 40 | {% endif %} 41 | {% if query.pop == '2' or query.pop == '4' or query.pop == '5' %} 42 | 49 | {% endif %} 50 | {% endif %} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /filebrowser/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /filebrowser/templatetags/fb_compat.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django import VERSION as DJANGO_VERSION 4 | from django import template 5 | from django.templatetags.static import static 6 | 7 | 8 | register = template.Library() 9 | 10 | def static_jquery(): 11 | if DJANGO_VERSION < (1, 9): 12 | return static("admin/js/jquery.min.js") 13 | 14 | return static("admin/js/vendor/jquery/jquery.min.js") 15 | 16 | register.simple_tag(static_jquery) 17 | 18 | 19 | def static_search_icon(): 20 | if DJANGO_VERSION < (1, 9): 21 | return static("admin/img/icon_searchbox.png") 22 | 23 | return static("admin/img/search.svg") 24 | 25 | register.simple_tag(static_search_icon) 26 | -------------------------------------------------------------------------------- /filebrowser/templatetags/fb_csrf.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.template import Library, Node 4 | from django.utils.safestring import mark_safe 5 | 6 | 7 | register = Library() 8 | 9 | 10 | class CsrfTokenNode(Node): 11 | def render(self, context): 12 | csrf_token = context.get('csrf_token', None) 13 | if csrf_token: 14 | if csrf_token == 'NOTPROVIDED': 15 | return mark_safe(u"") 16 | else: 17 | return mark_safe(u"
      " % (csrf_token)) 18 | else: 19 | # It's very probable that the token is missing because of 20 | # misconfiguration, so we raise a warning 21 | from django.conf import settings 22 | if settings.DEBUG: 23 | import warnings 24 | warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") 25 | return u'' 26 | 27 | 28 | def fb_csrf_token(parser, token): 29 | return CsrfTokenNode() 30 | register.tag(fb_csrf_token) 31 | -------------------------------------------------------------------------------- /filebrowser/templatetags/fb_pagination.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.template import Library 4 | 5 | 6 | register = Library() 7 | DOT = '.' 8 | 9 | 10 | @register.inclusion_tag('filebrowser/include/paginator.html', takes_context=True) 11 | def pagination(context): 12 | page_num = context['page'].number - 1 13 | paginator = context['p'] 14 | 15 | if not paginator.num_pages or paginator.num_pages == 1: 16 | page_range = [] 17 | else: 18 | ON_EACH_SIDE = 3 19 | ON_ENDS = 2 20 | 21 | # If there are 10 or fewer pages, display links to every page. 22 | # Otherwise, do some fancy 23 | if paginator.num_pages <= 10: 24 | page_range = range(paginator.num_pages) 25 | else: 26 | # Insert "smart" pagination links, so that there are always ON_ENDS 27 | # links at either end of the list of pages, and there are always 28 | # ON_EACH_SIDE links at either end of the "current page" link. 29 | page_range = [] 30 | if page_num > (ON_EACH_SIDE + ON_ENDS): 31 | page_range.extend(range(0, ON_EACH_SIDE - 1)) 32 | page_range.append(DOT) 33 | page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) 34 | else: 35 | page_range.extend(range(0, page_num + 1)) 36 | if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1): 37 | page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) 38 | page_range.append(DOT) 39 | page_range.extend(range(paginator.num_pages - ON_ENDS, paginator.num_pages)) 40 | else: 41 | page_range.extend(range(page_num + 1, paginator.num_pages)) 42 | 43 | return { 44 | 'page_range': page_range, 45 | 'page_num': page_num, 46 | 'filelisting': context['filelisting'], 47 | 'query': context['query'], 48 | } 49 | -------------------------------------------------------------------------------- /filebrowser/templatetags/fb_tags.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from urllib.parse import quote 3 | 4 | from django import template 5 | from django.template import TemplateSyntaxError 6 | from django.utils.safestring import mark_safe 7 | 8 | from filebrowser.settings import EXTENSIONS, SELECT_FORMATS 9 | 10 | 11 | register = template.Library() 12 | 13 | 14 | @register.inclusion_tag('filebrowser/include/_response.html', takes_context=True) 15 | def query_string(context, add=None, remove=None): 16 | """ 17 | Allows the addition and removal of query string parameters. 18 | 19 | _response.html is just {{ response }} 20 | 21 | Usage: 22 | http://www.url.com/{% query_string "param_to_add=value, param_to_add=value" "param_to_remove, params_to_remove" %} 23 | http://www.url.com/{% query_string "" "filter" %}filter={{new_filter}} 24 | http://www.url.com/{% query_string "sort=value" "sort" %} 25 | """ 26 | 27 | # Written as an inclusion tag to simplify getting the context. 28 | add = string_to_dict(add) 29 | remove = string_to_list(remove) 30 | params = context['query'].copy() 31 | response = get_query_string(params, add, remove) 32 | return {'response': response} 33 | 34 | 35 | def query_helper(query, add=None, remove=None): 36 | """ 37 | Helper Function for use within views. 38 | """ 39 | 40 | add = string_to_dict(add) 41 | remove = string_to_list(remove) 42 | params = query.copy() 43 | return get_query_string(params, add, remove) 44 | 45 | 46 | def get_query_string(p, new_params=None, remove=None): 47 | """ 48 | Add and remove query parameters. From `django.contrib.admin`. 49 | """ 50 | 51 | if new_params is None: 52 | new_params = {} 53 | if remove is None: 54 | remove = [] 55 | for r in remove: 56 | for k in list(p): 57 | if k == r: 58 | del p[k] 59 | for k, v in new_params.items(): 60 | if k in p and v is None: 61 | del p[k] 62 | elif v is not None: 63 | p[k] = v 64 | return '?' + '&'.join([u'%s=%s' % (quote(k), quote(v)) for k, v in p.items()]) 65 | 66 | 67 | def string_to_dict(string): 68 | """ 69 | Usage: 70 | {{ url|thumbnail:"width=10,height=20" }} 71 | {{ url|thumbnail:"width=10" }} 72 | {{ url|thumbnail:"height=20" }} 73 | """ 74 | 75 | kwargs = {} 76 | if string: 77 | string = str(string) 78 | if ',' not in string: 79 | # ensure at least one ',' 80 | string += ',' 81 | for arg in string.split(','): 82 | arg = arg.strip() 83 | if arg == '': 84 | continue 85 | kw, val = arg.split('=', 1) 86 | kwargs[kw] = val 87 | return kwargs 88 | 89 | 90 | def string_to_list(string): 91 | """ 92 | Usage: 93 | {{ url|thumbnail:"width,height" }} 94 | """ 95 | 96 | args = [] 97 | if string: 98 | string = str(string) 99 | if ',' not in string: 100 | # ensure at least one ',' 101 | string += ',' 102 | for arg in string.split(','): 103 | arg = arg.strip() 104 | if arg == '': 105 | continue 106 | args.append(arg) 107 | return args 108 | 109 | 110 | class SelectableNode(template.Node): 111 | def __init__(self, filetype, format): 112 | self.filetype = template.Variable(filetype) 113 | self.format = template.Variable(format) 114 | 115 | def render(self, context): 116 | try: 117 | filetype = self.filetype.resolve(context) 118 | except template.VariableDoesNotExist: 119 | filetype = '' 120 | try: 121 | format = self.format.resolve(context) 122 | except template.VariableDoesNotExist: 123 | format = '' 124 | if filetype and format and filetype in SELECT_FORMATS[format]: 125 | selectable = True 126 | elif filetype and format and filetype not in SELECT_FORMATS[format]: 127 | selectable = False 128 | else: 129 | selectable = True 130 | context['selectable'] = selectable 131 | return '' 132 | 133 | 134 | def selectable(parser, token): 135 | 136 | try: 137 | tag, filetype, format = token.split_contents() 138 | except: 139 | raise TemplateSyntaxError("%s tag requires 2 arguments" % token.contents.split()[0]) 140 | 141 | return SelectableNode(filetype, format) 142 | 143 | register.tag(selectable) 144 | 145 | 146 | def get_file_extensions(qs): 147 | extensions = [] 148 | if "type" in qs and qs.get("type") in SELECT_FORMATS: 149 | for format in SELECT_FORMATS.get(qs.get("type"), []): 150 | extensions.extend(EXTENSIONS[format]) 151 | else: 152 | for k, v in EXTENSIONS.items(): 153 | for item in v: 154 | if item: 155 | extensions.append(item) 156 | return mark_safe(extensions) 157 | 158 | 159 | # Django 1.9 auto escapes simple_tag unless marked as safe 160 | @register.simple_tag(name='get_file_extensions') 161 | def get_file_extensions_safe(qs): 162 | return mark_safe(get_file_extensions(qs)) 163 | -------------------------------------------------------------------------------- /filebrowser/templatetags/fb_versions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.conf import settings 4 | from django.core.files import File 5 | from django.template import Library, Node, Variable, VariableDoesNotExist, TemplateSyntaxError 6 | 7 | from filebrowser.settings import VERSIONS, PLACEHOLDER, SHOW_PLACEHOLDER, FORCE_PLACEHOLDER 8 | from filebrowser.base import FileObject 9 | from filebrowser.sites import get_default_site 10 | 11 | 12 | register = Library() 13 | 14 | 15 | class VersionNode(Node): 16 | def __init__(self, src, suffix, var_name): 17 | self.src = src 18 | self.suffix = suffix 19 | self.var_name = var_name 20 | 21 | def render(self, context): 22 | try: 23 | version_suffix = self.suffix.resolve(context) 24 | source = self.src.resolve(context) 25 | except VariableDoesNotExist: 26 | if self.var_name: 27 | return None 28 | return "" 29 | if version_suffix not in VERSIONS: 30 | return "" # FIXME: should this throw an error? 31 | if isinstance(source, FileObject): 32 | source = source.path 33 | elif isinstance(source, File): 34 | source = source.name 35 | else: # string 36 | source = source 37 | if 'filebrowser_site' in context: 38 | site = context['filebrowser_site'] 39 | else: 40 | site = get_default_site() 41 | if FORCE_PLACEHOLDER or (SHOW_PLACEHOLDER and not site.storage.isfile(source)): 42 | source = PLACEHOLDER 43 | fileobject = FileObject(source, site=site) 44 | try: 45 | version = fileobject.version_generate(version_suffix) 46 | if self.var_name: 47 | context[self.var_name] = version 48 | else: 49 | return version.url 50 | except Exception: 51 | if context.template.engine.debug: 52 | raise 53 | if self.var_name: 54 | context[self.var_name] = "" 55 | return "" 56 | 57 | 58 | def version(parser, token): 59 | """ 60 | Displaying a version of an existing Image according to the predefined VERSIONS settings (see filebrowser settings). 61 | {% version fileobject version_suffix %} 62 | 63 | Use {% version fileobject 'medium' %} in order to 64 | display the medium-size version of an image. 65 | version_suffix can be a string or a variable. if version_suffix is a string, use quotes. 66 | 67 | Return a context variable 'var_name' with the FileObject 68 | {% version fileobject version_suffix as var_name %} 69 | 70 | Use {% version fileobject 'medium' as version_medium %} in order to 71 | retrieve the medium version of an image stored in a variable version_medium. 72 | version_suffix can be a string or a variable. If version_suffix is a string, use quotes. 73 | """ 74 | 75 | bits = token.split_contents() 76 | if len(bits) != 3 and len(bits) != 5: 77 | raise TemplateSyntaxError("'version' tag takes 2 or 4 arguments") 78 | if len(bits) == 5 and bits[3] != 'as': 79 | raise TemplateSyntaxError("second argument to 'version' tag must be 'as'") 80 | if len(bits) == 3: 81 | return VersionNode(parser.compile_filter(bits[1]), parser.compile_filter(bits[2]), None) 82 | if len(bits) == 5: 83 | return VersionNode(parser.compile_filter(bits[1]), parser.compile_filter(bits[2]), bits[4]) 84 | 85 | 86 | class VersionSettingNode(Node): 87 | def __init__(self, version_suffix): 88 | if (version_suffix[0] == version_suffix[-1] and version_suffix[0] in ('"', "'")): 89 | self.version_suffix = version_suffix[1:-1] 90 | else: 91 | self.version_suffix = None 92 | self.version_suffix_var = Variable(version_suffix) 93 | 94 | def render(self, context): 95 | if self.version_suffix: 96 | version_suffix = self.version_suffix 97 | else: 98 | try: 99 | version_suffix = self.version_suffix_var.resolve(context) 100 | except VariableDoesNotExist: 101 | return None 102 | context['version_setting'] = VERSIONS[version_suffix] 103 | return '' 104 | 105 | 106 | def version_setting(parser, token): 107 | """ 108 | Get Information about a version setting. 109 | """ 110 | 111 | try: 112 | tag, version_suffix = token.split_contents() 113 | except: 114 | raise TemplateSyntaxError("%s tag requires 1 argument" % token.contents.split()[0]) 115 | if (version_suffix[0] == version_suffix[-1] and version_suffix[0] in ('"', "'")) and version_suffix.lower()[1:-1] not in VERSIONS: 116 | raise TemplateSyntaxError("%s tag received bad version_suffix %s" % (tag, version_suffix)) 117 | return VersionSettingNode(version_suffix) 118 | 119 | register.tag(version) 120 | register.tag(version_setting) 121 | -------------------------------------------------------------------------------- /filebrowser/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import re 4 | import os 5 | import unicodedata 6 | import math 7 | 8 | import six 9 | from django.utils.module_loading import import_string 10 | 11 | from filebrowser.settings import STRICT_PIL, NORMALIZE_FILENAME, CONVERT_FILENAME 12 | from filebrowser.settings import VERSION_PROCESSORS 13 | 14 | if STRICT_PIL: 15 | from PIL import Image 16 | else: 17 | try: 18 | from PIL import Image 19 | except ImportError: 20 | import Image 21 | 22 | 23 | # Handle Pillow 10 antialias deprecation 24 | ANTIALIAS = Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.ANTIALIAS 25 | 26 | 27 | def convert_filename(value): 28 | """ 29 | Convert Filename. 30 | """ 31 | 32 | if NORMALIZE_FILENAME: 33 | chunks = value.split(os.extsep) 34 | normalized = [] 35 | for v in chunks: 36 | v = unicodedata.normalize('NFKD', six.text_type(v)).encode('ascii', 'ignore').decode('ascii') 37 | v = re.sub(r'[^\w\s-]', '', v).strip() 38 | normalized.append(v) 39 | 40 | if len(normalized) > 1: 41 | value = '.'.join(normalized) 42 | else: 43 | value = normalized[0] 44 | 45 | if CONVERT_FILENAME: 46 | value = value.replace(" ", "_").lower() 47 | 48 | return value 49 | 50 | 51 | def path_strip(path, root): 52 | if not path or not root: 53 | return path 54 | path = os.path.normcase(path) 55 | root = os.path.normcase(root) 56 | if path.startswith(root): 57 | return path[len(root):] 58 | return path 59 | 60 | 61 | _default_processors = None 62 | 63 | 64 | def process_image(source, processor_options, processors=None): 65 | """ 66 | Process a source PIL image through a series of image processors, returning 67 | the (potentially) altered image. 68 | """ 69 | global _default_processors 70 | if processors is None: 71 | if _default_processors is None: 72 | _default_processors = [import_string(name) for name in VERSION_PROCESSORS] 73 | processors = _default_processors 74 | image = source 75 | for processor in processors: 76 | image = processor(image, **processor_options) 77 | return image 78 | 79 | 80 | def scale_and_crop(im, width=None, height=None, opts='', **kwargs): 81 | """ 82 | Scale and Crop. 83 | """ 84 | x, y = [float(v) for v in im.size] 85 | width = float(width or 0) 86 | height = float(height or 0) 87 | 88 | if (x, y) == (width, height): 89 | return im 90 | 91 | if 'upscale' not in opts: 92 | if (x < width or not width) and (y < height or not height): 93 | return im 94 | 95 | if width: 96 | xr = float(width) 97 | else: 98 | xr = float(x * height / y) 99 | if height: 100 | yr = float(height) 101 | else: 102 | yr = float(y * width / x) 103 | 104 | if 'crop' in opts: 105 | r = max(xr / x, yr / y) 106 | else: 107 | r = min(xr / x, yr / y) 108 | 109 | if r < 1.0 or (r > 1.0 and 'upscale' in opts): 110 | im = im.resize((int(math.ceil(x * r)), int(math.ceil(y * r))), resample=ANTIALIAS) 111 | 112 | if 'crop' in opts: 113 | x, y = [float(v) for v in im.size] 114 | ex, ey = (x - min(x, xr)) / 2, (y - min(y, yr)) / 2 115 | if ex or ey: 116 | im = im.crop((int(ex), int(ey), int(ex + xr), int(ey + yr))) 117 | return im 118 | 119 | scale_and_crop.valid_options = ('crop', 'upscale') 120 | -------------------------------------------------------------------------------- /manual_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Auto setup project for Django >=2 6 | 7 | VIRTUALENV_DIR="envs" 8 | PROJECTS_DIR="projects" 9 | BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 10 | SED=sed 11 | if [[ "$OSTYPE" == "darwin"* ]]; then 12 | SED=gsed 13 | fi 14 | 15 | echo "prepare projects for manual testing" 16 | source "$VIRTUALENV_DIR/$1/bin/activate" 17 | 18 | PROJECT_PATH="$PROJECTS_DIR/fb-dj$1" 19 | rm -rf $PROJECT_PATH 20 | mkdir -p $PROJECT_PATH 21 | django-admin startproject fb $PROJECT_PATH 22 | ln -s "$BASEDIR/filebrowser" "$PROJECT_PATH/filebrowser" 23 | 24 | echo " 25 | INSTALLED_APPS = ( 26 | 'django.contrib.admin', 27 | 'django.contrib.auth', 28 | 'django.contrib.contenttypes', 29 | 'django.contrib.sessions', 30 | 'django.contrib.messages', 31 | 'django.contrib.staticfiles', 32 | 'filebrowser', 33 | #'dummy', 34 | ) 35 | 36 | import os.path 37 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 38 | MEDIA_URL = '/media/' 39 | " >> "$PROJECT_PATH/fb/settings.py" 40 | 41 | echo " 42 | from django.conf import settings 43 | from django.conf.urls.static import static 44 | from django.contrib import admin 45 | from django.urls import path 46 | from filebrowser.sites import site 47 | 48 | urlpatterns = [ 49 | path('admin/filebrowser/', site.urls), 50 | path('admin/', admin.site.urls), 51 | ] 52 | 53 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 54 | " > "$PROJECT_PATH/fb/urls.py" 55 | 56 | ( 57 | cd $PROJECT_PATH 58 | mkdir -p media/uploads/ 59 | mkdir -p media/_versions/ 60 | 61 | python manage.py test 62 | 63 | python manage.py startapp dummy 64 | 65 | echo " 66 | from django.db import models 67 | from filebrowser.fields import FileBrowseField, FileBrowseUploadField 68 | 69 | class DemoItem(models.Model): 70 | title = models.CharField('Title', max_length=210) 71 | attach = FileBrowseField('Attach', max_length=200) 72 | upload = FileBrowseUploadField('Attach', max_length=200) 73 | " > dummy/models.py 74 | 75 | echo " 76 | from django.contrib import admin 77 | from dummy.models import DemoItem 78 | 79 | class DemoAdmin(admin.ModelAdmin): 80 | pass 81 | 82 | admin.site.register(DemoItem, DemoAdmin) 83 | " > dummy/admin.py 84 | 85 | $SED -i "s/#'dummy/'dummy/" fb/settings.py 86 | 87 | python manage.py makemigrations 88 | 89 | python manage.py migrate 90 | python manage.py createsuperuser --noinput --username admin --email admin@sample.com 91 | ) -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 7 | 8 | # Django >=1.8 9 | import django 10 | if getattr(django, 'setup', False): 11 | django.setup() 12 | 13 | from django.conf import settings 14 | from django.test.utils import get_runner 15 | 16 | TestRunner = get_runner(settings) 17 | test_runner = TestRunner() 18 | failures = test_runner.run_tests(["tests"]) 19 | sys.exit(bool(failures)) 20 | -------------------------------------------------------------------------------- /runtests_in_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DJANGO_VERSIONS=("2.0" "3.0" "4.0") 4 | VIRTUALENV_DIR="envs" 5 | BASEDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 6 | 7 | mkdir -p $VIRTUALENV_DIR 8 | 9 | for version in "${DJANGO_VERSIONS[@]}" 10 | do 11 | echo "--------" 12 | echo "DJANGO $version" 13 | echo "--------" 14 | 15 | echo "install last django" 16 | if [ ! -d "$VIRTUALENV_DIR/$version" ]; then 17 | virtualenv --system-site-packages --python=python3 "$VIRTUALENV_DIR/$version" 18 | fi 19 | source "$VIRTUALENV_DIR/$version/bin/activate" 20 | pip install "django~=$version.0" 21 | 22 | echo "install test dependencies" 23 | pip install -r "$BASEDIR/tests/requirements.txt" 24 | 25 | echo "run filebrowser test" 26 | python "$BASEDIR/runtests.py" 27 | done 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | 9 | setup( 10 | name='django-filebrowser-no-grappelli', 11 | version='4.0.2', 12 | description='Media-Management no Grappelli', 13 | long_description=read('README.rst'), 14 | url='https://github.com/smacker/django-filebrowser-no-grappelli', 15 | download_url='', 16 | author='Patrick Kranzlmueller, Axel Swoboda (vonautomatisch)', 17 | author_email='office@vonautomatisch.at', 18 | maintainer='Maxim Sukharev', 19 | maintainer_email='max@smacker.ru', 20 | license='BSD', 21 | packages=find_packages(exclude=['tests']), 22 | include_package_data=True, 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Environment :: Web Environment', 26 | 'Framework :: Django', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.7', 38 | 'Programming Language :: Python :: 3.8', 39 | ], 40 | zip_safe=False, 41 | install_requires=['six'], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.contrib.auth import get_user_model 6 | from django.test import TestCase 7 | 8 | from filebrowser.settings import DIRECTORY, VERSIONS_BASEDIR 9 | from filebrowser.base import FileObject 10 | from filebrowser.sites import site 11 | 12 | 13 | class FilebrowserTestCase(TestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | super(FilebrowserTestCase, cls).setUpClass() 18 | User = get_user_model() 19 | cls.user = User.objects.create_user('testuser', 'test@domain.com', 'password') 20 | cls.user.is_staff = True 21 | cls.user.save() 22 | 23 | def setUp(self): 24 | self.DIRECTORY = DIRECTORY 25 | self.TEST_PATH = os.path.join(site.storage.location, '_test') 26 | self.DIRECTORY_PATH = os.path.join(site.storage.location, DIRECTORY) 27 | self.VERSIONS_PATH = os.path.join(site.storage.location, VERSIONS_BASEDIR) 28 | 29 | if os.path.exists(self.TEST_PATH): 30 | raise Exception('TEST_PATH Already Exists') 31 | 32 | self.TEMP_PATH = os.path.join(self.TEST_PATH, 'tempfolder') 33 | self.FOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'folder') 34 | self.SUBFOLDER_PATH = os.path.join(self.FOLDER_PATH, 'subfolder') 35 | self.CREATEFOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'create') 36 | self.PLACEHOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'placeholders') 37 | 38 | self.STATIC_IMG_PATH = os.path.join(settings.BASE_DIR, 'filebrowser', "static", "filebrowser", "img", "testimage.jpg") 39 | self.STATIC_IMG_BAD_NAME_PATH = os.path.join(settings.BASE_DIR, 'filebrowser', "static", "filebrowser", "img", "TEST_IMAGE_000.jpg") 40 | 41 | self.F_IMAGE = FileObject(os.path.join(DIRECTORY, 'folder', "testimage.jpg"), site=site) 42 | self.F_MISSING = FileObject(os.path.join(DIRECTORY, 'folder', "missing.jpg"), site=site) 43 | self.F_FOLDER = FileObject(os.path.join(DIRECTORY, 'folder'), site=site) 44 | self.F_SUBFOLDER = FileObject(os.path.join(DIRECTORY, 'folder', 'subfolder'), site=site) 45 | self.F_CREATEFOLDER = FileObject(os.path.join(DIRECTORY, 'create'), site=site) 46 | self.F_TEMPFOLDER = FileObject(os.path.join('_test', 'tempfolder'), site=site) 47 | 48 | os.makedirs(self.FOLDER_PATH) 49 | os.makedirs(self.SUBFOLDER_PATH) 50 | 51 | def tearDown(self): 52 | shutil.rmtree(self.TEST_PATH) 53 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.models import User 6 | from django.test import TestCase 7 | 8 | from filebrowser.settings import DIRECTORY, VERSIONS_BASEDIR 9 | from filebrowser.base import FileObject 10 | from filebrowser.sites import site 11 | 12 | 13 | class FilebrowserTestCase(TestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | super(FilebrowserTestCase, cls).setUpClass() 18 | 19 | if not User.objects.filter(username='testuser').count(): 20 | cls.user = User.objects.create_user('testuser', 'test@domain.com', 'password') 21 | cls.user.is_staff = True 22 | cls.user.save() 23 | else: 24 | cls.user = User.objects.get(username='testuser') 25 | 26 | def setUp(self): 27 | self.DIRECTORY = DIRECTORY 28 | self.TEST_PATH = os.path.join(site.storage.location, '_test') 29 | self.DIRECTORY_PATH = os.path.join(site.storage.location, DIRECTORY) 30 | self.VERSIONS_PATH = os.path.join(site.storage.location, VERSIONS_BASEDIR) 31 | 32 | if os.path.exists(self.TEST_PATH): 33 | raise Exception('TEST_PATH Already Exists') 34 | 35 | self.TEMP_PATH = os.path.join(self.TEST_PATH, 'tempfolder') 36 | self.FOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'folder') 37 | self.SUBFOLDER_PATH = os.path.join(self.FOLDER_PATH, 'subfolder') 38 | self.CREATEFOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'create') 39 | self.PLACEHOLDER_PATH = os.path.join(self.DIRECTORY_PATH, 'placeholders') 40 | 41 | self.STATIC_IMG_PATH = os.path.join(settings.BASE_DIR, 'filebrowser', "static", "filebrowser", "img", "testimage.jpg") 42 | self.STATIC_IMG_BAD_NAME_PATH = os.path.join(settings.BASE_DIR, 'filebrowser', "static", "filebrowser", "img", "TEST_IMAGE_000.jpg") 43 | self.STATIC_IMG_BAD_PATH = os.path.join(settings.BASE_DIR, 'filebrowser', "static", "filebrowser", "img", "test_not_an_image.jpg") 44 | 45 | self.F_IMAGE = FileObject(os.path.join(DIRECTORY, 'folder', "testimage.jpg"), site=site) 46 | self.F_IMAGE_BAD = FileObject(os.path.join(DIRECTORY, 'folder', "test_not_an_image.jpg"), site=site) 47 | self.F_MISSING = FileObject(os.path.join(DIRECTORY, 'folder', "missing.jpg"), site=site) 48 | self.F_FOLDER = FileObject(os.path.join(DIRECTORY, 'folder'), site=site) 49 | self.F_SUBFOLDER = FileObject(os.path.join(DIRECTORY, 'folder', 'subfolder'), site=site) 50 | self.F_CREATEFOLDER = FileObject(os.path.join(DIRECTORY, 'create'), site=site) 51 | self.F_TEMPFOLDER = FileObject(os.path.join('_test', 'tempfolder'), site=site) 52 | 53 | os.makedirs(self.FOLDER_PATH) 54 | os.makedirs(self.SUBFOLDER_PATH) 55 | 56 | def tearDown(self): 57 | shutil.rmtree(self.TEST_PATH) 58 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from .test_base import * 2 | from .test_commands import * 3 | from .test_decorators import * 4 | from .test_settings import * 5 | from .test_sites import * 6 | from .test_templatetags import * 7 | from .test_versions import * 8 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | funcsigs==1.0.2 2 | mock==3.0.5 3 | pbr==5.4.4 4 | Pillow==5.4.1; python_version < '3.5' 5 | Pillow==6.2.1; python_version >= '3.5' 6 | six==1.12.0 7 | wheel==0.33.6 8 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tests project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '_4wa$(v5wpr7q(_8h1-1xs^0inp62jav%1$cuo0$r54h%17m^^' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'filebrowser', 40 | 'django.contrib.admin', 41 | 'tests', 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = MIDDLEWARE = ( 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'tests.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | 71 | # WSGI_APPLICATION = 'tests.wsgi.application' 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | 84 | # Internationalization 85 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 86 | 87 | LANGUAGE_CODE = 'en-us' 88 | 89 | TIME_ZONE = 'UTC' 90 | 91 | USE_I18N = True 92 | 93 | USE_L10N = True 94 | 95 | USE_TZ = True 96 | 97 | 98 | # Static files (CSS, JavaScript, Images) 99 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 100 | 101 | STATIC_URL = '/static/' 102 | 103 | # Settings added for for django-filebrowser tests 104 | MEDIA_ROOT = os.path.join(BASE_DIR, 'tests', 'media') 105 | MEDIA_URL = '/media/' 106 | 107 | FILEBROWSER_DIRECTORY = '_test/uploads/' 108 | FILEBROWSER_VERSIONS_BASEDIR = '_test/_versions/' 109 | FILEBROWSER_PLACEHOLDER = '_test/uploads/placeholders/testimage.jpg' 110 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import sys 5 | import shutil 6 | 7 | from django.conf import settings 8 | from django.core.management import call_command 9 | from six import StringIO 10 | 11 | from filebrowser.settings import DIRECTORY 12 | from tests.base import FilebrowserTestCase as TestCase 13 | 14 | 15 | class VersionGenerateCommandTests(TestCase): 16 | 17 | def setUp(self): 18 | super(VersionGenerateCommandTests, self).setUp() 19 | shutil.copy(self.STATIC_IMG_PATH, self.FOLDER_PATH) 20 | self.version_file = os.path.join(settings.MEDIA_ROOT, "_test/_versions/folder/testimage_large.jpg") 21 | # self._stdout = sys.stdout 22 | 23 | def tearDown(self): 24 | super(VersionGenerateCommandTests, self).tearDown() 25 | # sys.stdout = self._stdout 26 | 27 | def test_fb_version_generate(self): 28 | self.assertFalse(os.path.exists(self.version_file)) 29 | 30 | # sys.stdout = open(os.devnull, 'wb') 31 | sys.stdin = StringIO("large") 32 | 33 | call_command('fb_version_generate', DIRECTORY) 34 | 35 | self.assertTrue(os.path.exists(self.version_file)) 36 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import shutil 4 | 5 | from filebrowser.decorators import get_path, get_file 6 | from filebrowser.sites import site 7 | from tests.base import FilebrowserTestCase as TestCase 8 | 9 | 10 | class GetPathTests(TestCase): 11 | 12 | def test_empty(self): 13 | self.assertEqual(get_path('', site), '') 14 | 15 | def test_starts_with_period(self): 16 | self.assertIsNone(get_path('..', site)) 17 | self.assertIsNone(get_path('.filter/subfolder', site)) 18 | 19 | def test_is_absolute(self): 20 | self.assertIsNone(get_path('/etc/password', site)) 21 | self.assertIsNone(get_path('/folder/subfolder', site)) 22 | 23 | def test_does_not_exist(self): 24 | self.assertIsNone(get_path('folder/invalid', site)) 25 | 26 | def test_valid(self): 27 | self.assertTrue(get_path('folder/subfolder', site)) 28 | 29 | 30 | class GetFileTests(TestCase): 31 | 32 | def test_empty(self): 33 | # TODO: Should this return '' or 'folder' 34 | self.assertEqual(get_file('folder', '', site), '') 35 | 36 | def test_starts_with_period(self): 37 | self.assertIsNone(get_file('.', 'folder/subfolder', site)) 38 | 39 | def test_filename_starts_with_period(self): 40 | self.assertIsNone(get_file('folder', '../folder/', site)) 41 | 42 | def test_is_absolute(self): 43 | self.assertIsNone(get_file('/etc/', 'password', site)) 44 | self.assertIsNone(get_file('/folder/subfolder', '', site)) 45 | 46 | def test_filename_is_absolute(self): 47 | self.assertIsNone(get_file('folder/subfolder', '/etc/', site)) 48 | 49 | def test_does_not_exist(self): 50 | self.assertIsNone(get_file('folder', 'invalid', site)) 51 | self.assertIsNone(get_file('folder', 'invalid.jpg', site)) 52 | 53 | def test_valid_folder(self): 54 | self.assertTrue(get_file('folder', 'subfolder', site)) 55 | 56 | def test_valid_file(self): 57 | self.assertIsNone(get_file('folder/subfolder', 'testimage.jpg', site)) 58 | shutil.copy(self.STATIC_IMG_PATH, self.SUBFOLDER_PATH) 59 | self.assertTrue(get_file('folder/subfolder', 'testimage.jpg', site)) 60 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from tests import FilebrowserTestCase as TestCase 4 | 5 | from six import string_types 6 | 7 | from filebrowser.base import FileObject 8 | from filebrowser.fields import FileBrowseField 9 | 10 | 11 | class FileBrowseFieldTests(TestCase): 12 | path_to_file = "/path/to/file.jpg" 13 | 14 | def test_to_python(self): 15 | field = FileBrowseField() 16 | 17 | # should deal gracefully with any of the following arguments: 18 | # An instance of the correct type, a string, and None (if the field allows null=True) 19 | values = [FileObject(self.path_to_file), self.path_to_file] 20 | for v in values: 21 | actual = field.to_python(v) 22 | self.assertIsInstance(actual, FileObject) 23 | self.assertEqual(actual.path, self.path_to_file) 24 | self.assertEqual(field.to_python(None), None) 25 | 26 | def test_get_prep_value(self): 27 | field = FileBrowseField() 28 | 29 | # similar to to_python() should handle all 3 cases 30 | values = [FileObject(self.path_to_file), self.path_to_file] 31 | for v in values: 32 | actual = field.get_prep_value(v) 33 | self.assertIsInstance(actual, string_types) 34 | self.assertEqual(actual, self.path_to_file) 35 | self.assertEqual(field.to_python(None), None) 36 | -------------------------------------------------------------------------------- /tests/test_namers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import shutil 4 | 5 | from mock import patch 6 | 7 | from filebrowser.settings import VERSIONS 8 | from tests import FilebrowserTestCase as TestCase 9 | 10 | from filebrowser.namers import OptionsNamer 11 | 12 | 13 | class BaseNamerTests(TestCase): 14 | NAMER_CLASS = OptionsNamer 15 | 16 | def setUp(self): 17 | super(BaseNamerTests, self).setUp() 18 | shutil.copy(self.STATIC_IMG_PATH, self.FOLDER_PATH) 19 | 20 | def _get_namer(self, version_suffix, file_object=None, **extra_options): 21 | if not file_object: 22 | file_object = self.F_IMAGE 23 | 24 | extra_options.update(VERSIONS.get(version_suffix, {})) 25 | 26 | return self.NAMER_CLASS( 27 | file_object=file_object, 28 | version_suffix=version_suffix, 29 | filename_root=file_object.filename_root, 30 | extension=file_object.extension, 31 | options=extra_options, 32 | ) 33 | 34 | 35 | class OptionsNamerTests(BaseNamerTests): 36 | 37 | def test_should_return_correct_version_name_using_predefined_versions(self): 38 | expected = [ 39 | ("admin_thumbnail", "testimage_admin-thumbnail--60x60--opts-crop.jpg", ), 40 | ("thumbnail", "testimage_thumbnail--60x60--opts-crop.jpg", ), 41 | ("small", "testimage_small--140x0.jpg", ), 42 | ("medium", "testimage_medium--300x0.jpg", ), 43 | ("big", "testimage_big--460x0.jpg", ), 44 | ("large", "testimage_large--680x0.jpg", ), 45 | ] 46 | 47 | for version_suffix, expected_name in expected: 48 | namer = self._get_namer(version_suffix) 49 | self.assertEqual(namer.get_version_name(), expected_name) 50 | 51 | @patch('filebrowser.namers.VERSION_NAMER', 'filebrowser.namers.OptionsNamer') 52 | def test_should_return_correct_original_name_using_predefined_versions(self): 53 | expected = 'testimage.jpg' 54 | for version_suffix in VERSIONS.keys(): 55 | file_object = self.F_IMAGE.version_generate(version_suffix) 56 | namer = self._get_namer(version_suffix, file_object) 57 | self.assertNotEqual(file_object.filename, expected) 58 | 59 | self.assertEqual(namer.get_original_name(), expected) 60 | self.assertEqual(file_object.original_filename, expected) 61 | 62 | def test_should_append_extra_options(self): 63 | expected = [ 64 | ( 65 | "thumbnail", 66 | "testimage_thumbnail--60x60--opts-crop--sepia.jpg", 67 | {'sepia': True} 68 | ), ( 69 | "small", 70 | "testimage_small--140x0--thumb--transparency-08.jpg", 71 | {'transparency': 0.8, 'thumb': True} 72 | ), ( 73 | "large", 74 | "testimage_large--680x0--nested-xpto-ops--thumb.jpg", 75 | {'thumb': True, 'nested': {'xpto': 'ops'}} 76 | ), 77 | ] 78 | 79 | for version_suffix, expected_name, extra_options in expected: 80 | namer = self._get_namer(version_suffix, **extra_options) 81 | self.assertEqual(namer.get_version_name(), expected_name) 82 | 83 | def test_generated_version_name_options_list_should_be_ordered(self): 84 | "the order is important to always generate the same name" 85 | expected = [ 86 | ( 87 | "small", 88 | "testimage_small--140x0--a--x-crop--z-4.jpg", 89 | {'z': 4, 'a': True, 'x': 'crop'} 90 | ), 91 | ] 92 | 93 | for version_suffix, expected_name, extra_options in expected: 94 | namer = self._get_namer(version_suffix, **extra_options) 95 | self.assertEqual(namer.get_version_name(), expected_name) 96 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # FIXME 4 | # These tests are designed to run against production, not testing. They 5 | # would be better suited for the Django Check Framework. 6 | # 7 | # https://docs.djangoproject.com/en/1.8/topics/checks/ 8 | 9 | 10 | # # PYTHON IMPORTS 11 | # import os 12 | 13 | # # DJANGO IMPORTS 14 | # from django.test import TestCase 15 | # from django.conf import settings 16 | 17 | # # FILEBROWSER IMPORTS 18 | # from filebrowser.settings import * 19 | 20 | 21 | # class SettingsTests(TestCase): 22 | 23 | # def setUp(self): 24 | # pass 25 | 26 | # def test_directory(self): 27 | # """ 28 | # Test that ``MEDIA_ROOT`` plus ``DIRECTORY`` exists. 29 | # """ 30 | # self.assertEqual(os.path.exists(os.path.join(settings.MEDIA_ROOT, DIRECTORY)), 1) 31 | # # Check for trailing slash 32 | # self.assertEqual(os.path.basename(DIRECTORY), '') 33 | 34 | # def test_versions_basedir(self): 35 | # """ 36 | # Test that ``MEDIA_ROOT`` plus ``VERSIONS_BASEDIR`` exists. 37 | # """ 38 | # self.assertEqual(os.path.exists(os.path.join(settings.MEDIA_ROOT, VERSIONS_BASEDIR)), 1) 39 | 40 | # def test_admin_thumbnail(self): 41 | # """ 42 | # Test if ``ADMIN_THUMBNAIL`` is set and is part of ``VERSIONS``. 43 | # """ 44 | # self.assertNotEqual(ADMIN_THUMBNAIL, '') 45 | # self.assertIn(ADMIN_THUMBNAIL, VERSIONS) 46 | 47 | # def test_admin_versions(self): 48 | # """ 49 | # Test if ``ADMIN_VERSIONS`` are part of ``VERSIONS``. 50 | # """ 51 | # for item in ADMIN_VERSIONS: 52 | # self.assertIn(item, VERSIONS) 53 | 54 | # def test_placeholder(self): 55 | # """ 56 | # Test if ``PLACEHOLDER`` exists. 57 | # """ 58 | # self.assertEqual(os.path.exists(os.path.join(settings.MEDIA_ROOT, PLACEHOLDER)), 1) 59 | 60 | # def test_show_placeholder(self): 61 | # """ 62 | # Test if ``SHOW_PLACEHOLDER`` is in ``True, False``. 63 | # """ 64 | # self.assertIn(SHOW_PLACEHOLDER, [True, False]) 65 | 66 | # def test_force_placeholder(self): 67 | # """ 68 | # Test if ``FORCE_PLACEHOLDER`` is in ``True, False``. 69 | # """ 70 | # self.assertIn(FORCE_PLACEHOLDER, [True, False]) 71 | 72 | # def test_strict_pil(self): 73 | # """ 74 | # Test if ``STRICT_PIL`` is in ``True, False``. 75 | # """ 76 | # self.assertIn(STRICT_PIL, [True, False]) 77 | 78 | # def test_normalize_filename(self): 79 | # """ 80 | # Test if ``NORMALIZE_FILENAME`` is in ``True, False``. 81 | # """ 82 | # self.assertIn(NORMALIZE_FILENAME, [True, False]) 83 | 84 | # def test_convert_filename(self): 85 | # """ 86 | # Test if ``CONVERT_FILENAME`` is in ``True, False``. 87 | # """ 88 | # self.assertIn(CONVERT_FILENAME, [True, False]) 89 | 90 | # def test_default_sorting_by(self): 91 | # """ 92 | # Test if ``DEFAULT_SORTING_BY`` is in ``date, filesize, filename_lower, filetype_checked``. 93 | # """ 94 | # self.assertIn(DEFAULT_SORTING_BY, ['date', 'filesize', 'filename_lower', 'filetype_checked']) 95 | 96 | # def test_default_sorting_order(self): 97 | # """ 98 | # Test if ``DEFAULT_SORTING_ORDER`` is in ``asc, desc``. 99 | # """ 100 | # self.assertIn(DEFAULT_SORTING_ORDER, ['asc', 'desc']) 101 | 102 | # def test_search_traverse(self): 103 | # """ 104 | # Test if ``SEARCH_TRAVERSE`` is in ``True, False``. 105 | # """ 106 | # self.assertIn(SEARCH_TRAVERSE, [True, False]) 107 | 108 | # def test_overwrite_existing(self): 109 | # """ 110 | # Test if ``OVERWRITE_EXISTING`` is in ``True, False``. 111 | # """ 112 | # self.assertIn(OVERWRITE_EXISTING, [True, False]) 113 | -------------------------------------------------------------------------------- /tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.test import TestCase 3 | from django.http import QueryDict 4 | 5 | from filebrowser.templatetags.fb_tags import get_file_extensions 6 | 7 | 8 | class GetFileExtensionsTemplateTagTests(TestCase): 9 | def test_get_all(self): 10 | self.assertEqual( 11 | sorted(eval(get_file_extensions(''))), 12 | sorted([ 13 | '.pdf', '.doc', '.rtf', '.txt', '.xls', '.csv', '.docx', '.mov', 14 | '.wmv', '.mpeg', '.mpg', '.avi', '.rm', '.jpg', '.jpeg', '.gif', '.png', 15 | '.tif', '.tiff', '.mp3', '.mp4', '.wav', '.aiff', '.midi', '.m4p', '.m4v', '.webm' 16 | ])) 17 | 18 | def test_get_filtered(self): 19 | self.assertEqual( 20 | get_file_extensions(QueryDict('type=image')), 21 | "['.jpg', '.jpeg', '.gif', '.png', '.tif', '.tiff']" 22 | ) 23 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import re_path 3 | from django.contrib import admin 4 | from django import VERSION as DJANGO_VERSION 5 | 6 | from filebrowser.sites import site 7 | 8 | admin.autodiscover() 9 | 10 | if DJANGO_VERSION >= (1, 9): 11 | urlpatterns = [ 12 | re_path(r'^admin/filebrowser/', include(site.urls[:2], namespace=site.urls[2])), 13 | re_path(r'^admin/', include(admin.site.urls[:2], namespace=admin.site.urls[2])), 14 | ] 15 | else: 16 | urlpatterns = [ 17 | re_path(r'^admin/filebrowser/', include(site.urls)), 18 | re_path(r'^admin/', include(admin.site.urls)), 19 | ] 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35,36}-django111,py{35,36,37}-django22,py{36,37,38}-django30 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONDONTWRITEBYTECODE=1 7 | deps = 8 | django111: Django>=1.11,<2.0 9 | django22: Django>=2.2,<3.0 10 | django30: Django>=3.0,<3.1 11 | -rtests/requirements.txt 12 | coverage 13 | commands = ./runtests.py {posargs} 14 | --------------------------------------------------------------------------------