├── docs ├── dummy │ ├── __init__.py │ ├── media_backends.py │ └── settings.py ├── .gitignore ├── _static │ └── admin-screenshot.png ├── admin.rst ├── utils.rst ├── models.rst ├── bundled_extensions.rst ├── management.rst ├── bundled_plugins.rst ├── views.rst ├── templates.rst ├── fields.rst ├── index.rst ├── custom_plugins.rst └── configuration.rst ├── media_tree ├── contrib │ ├── __init__.py │ ├── media_backends │ │ ├── __init__.py │ │ ├── sorl_thumbnail │ │ │ └── __init__.py │ │ ├── vector_image │ │ │ └── __init__.py │ │ └── easy_thumbnails │ │ │ └── __init__.py │ ├── media_extensions │ │ ├── __init__.py │ │ ├── images │ │ │ ├── __init__.py │ │ │ └── focal_point │ │ │ │ ├── static │ │ │ │ └── focal_point │ │ │ │ │ ├── img │ │ │ │ │ └── focal_point.png │ │ │ │ │ ├── css │ │ │ │ │ └── focal_point.css │ │ │ │ │ └── js │ │ │ │ │ └── focal_point.js │ │ │ │ ├── __init__.py │ │ │ │ └── media_extension.py │ │ └── zipfiles │ │ │ ├── __init__.py │ │ │ ├── zip_operations.py │ │ │ └── media_extension.py │ ├── legacy_mptt_support │ │ ├── __init__.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── media_tree_admin.py │ │ └── templates │ │ │ └── admin │ │ │ ├── mptt_change_list.html │ │ │ └── mptt_change_list_results.html │ ├── media_tree_modeltranslation │ │ ├── __init__.py │ │ └── translation.py │ ├── cms_plugins │ │ ├── media_tree_image │ │ │ ├── templates │ │ │ │ └── cms │ │ │ │ │ └── plugins │ │ │ │ │ └── media_tree_image.html │ │ │ ├── urls.py │ │ │ ├── cms_app.py │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ ├── views.py │ │ │ └── cms_plugins.py │ │ ├── media_tree_listing │ │ │ ├── templates │ │ │ │ └── cms │ │ │ │ │ └── plugins │ │ │ │ │ └── media_tree_listing.html │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ └── cms_plugins.py │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── media_tree_slideshow │ │ │ ├── settings.py │ │ │ ├── __init__.py │ │ │ ├── templates │ │ │ │ └── cms │ │ │ │ │ └── plugins │ │ │ │ │ └── media_tree_slideshow.html │ │ │ ├── cms_plugins.py │ │ │ └── models.py │ │ ├── media_tree_gallery │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ ├── cms_plugins.py │ │ │ └── templates │ │ │ │ └── cms │ │ │ │ └── plugins │ │ │ │ └── media_tree_gallery.html │ │ ├── helpers.py │ │ ├── forms.py │ │ └── static │ │ │ └── media_tree │ │ │ └── js │ │ │ └── jquery-inline-positioning.js │ └── views │ │ ├── templates │ │ └── media_tree │ │ │ ├── base.html │ │ │ ├── filenode_list.html │ │ │ ├── filenode_detail.html │ │ │ └── image_detail.html │ │ ├── __init__.py │ │ ├── helpers.py │ │ ├── mixin_base.py │ │ └── detail │ │ ├── image.py │ │ └── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── mediacache.py │ │ └── mediaorphaned.py ├── migrations │ ├── __init__.py │ ├── 0003_mptt_to_treebeard_data.py │ └── 0002_mptt_to_treebeard.py ├── admin │ ├── actions │ │ ├── __init__.py │ │ └── utils.py │ ├── __init__.py │ ├── utils.py │ └── change_list.py ├── templatetags │ ├── __init__.py │ ├── media_tree_admin.py │ └── media_tree_tags.py ├── south_migrations │ └── __init__.py ├── __init__.py ├── templates │ ├── media_tree │ │ └── filenode │ │ │ └── includes │ │ │ ├── figure.html │ │ │ ├── preview.html │ │ │ ├── icon.html │ │ │ ├── thumbnail.html │ │ │ └── figure_base.html │ └── admin │ │ └── media_tree │ │ └── filenode │ │ ├── includes │ │ ├── preview.html │ │ ├── widget_preview.html │ │ ├── list_type_controls.html │ │ ├── thumbnail_size_controls.html │ │ ├── breadcrumbs.html │ │ └── uploader_button_js.html │ │ ├── flat_change_list_results.html │ │ ├── change_form.html │ │ ├── upload_form.html │ │ ├── tree_change_list.html │ │ └── actions_form.html ├── static │ └── media_tree │ │ ├── js │ │ ├── jquery.init.js │ │ └── filenode_foreignkeywidget.js │ │ ├── img │ │ └── icons │ │ │ ├── copy.png │ │ │ ├── drag-handler.png │ │ │ ├── folder-toggle.png │ │ │ ├── mimetypes │ │ │ ├── _blank.png │ │ │ ├── _folder.png │ │ │ └── _folder_expanded.png │ │ │ └── folder-toggle-loading.gif │ │ ├── lib │ │ └── jquery.fineuploader-4.4.0 │ │ │ ├── edit.gif │ │ │ ├── loading.gif │ │ │ ├── processing.gif │ │ │ ├── placeholders │ │ │ ├── waiting-generic.png │ │ │ └── not_available-generic.png │ │ │ ├── iframe.xss.response-4.4.0.js │ │ │ ├── templates │ │ │ ├── default.html │ │ │ └── simple-thumbnails.html │ │ │ └── fineuploader-4.4.0.min.css │ │ └── css │ │ └── filenode_preview.css ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ ├── djangojs.mo │ │ │ └── djangojs.po │ └── nb │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ ├── djangojs.mo │ │ └── djangojs.po ├── media_types.py ├── tests │ └── __init__,py ├── extension │ ├── __init__.py │ └── base_extenders │ │ ├── form_extender.py │ │ ├── admin_extender.py │ │ ├── __init__.py │ │ └── model_extender.py ├── utils │ ├── maintenance.py │ ├── __init__.py │ └── staticfiles.py ├── media_backends │ └── __init__.py ├── forms.py ├── widgets.py └── fields.py ├── demo_project ├── demo_project │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── .gitignore ├── media │ └── upload │ │ ├── River.jpg │ │ ├── Sunset.jpg │ │ ├── Building.jpg │ │ ├── Coconut_Trees.jpg │ │ └── license.txt ├── requirements.txt ├── manage.py └── templates │ └── media_tree │ └── base.html ├── .coveragerc ├── requirements.txt ├── setup.cfg ├── .gitignore ├── AUTHORS ├── IDEAS.txt ├── .travis.yml ├── MANIFEST.in ├── tox.ini ├── LICENSE ├── setup.py └── README.rst /docs/dummy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_project/demo_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | build 3 | _build -------------------------------------------------------------------------------- /media_tree/admin/actions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo_project/.gitignore: -------------------------------------------------------------------------------- 1 | static 2 | db.sqlite3 -------------------------------------------------------------------------------- /media_tree/contrib/media_backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/contrib/legacy_mptt_support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/contrib/media_tree_modeltranslation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media_tree/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from .filenode_admin import * 2 | -------------------------------------------------------------------------------- /media_tree/contrib/legacy_mptt_support/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = media_tree 3 | omit = media_tree/tests/* 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.6 2 | South>=1.0 3 | Pillow>=2.3 4 | django-treebeard>=4.0 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = build,dist,docs,*/migrations,*.egg-info 4 | -------------------------------------------------------------------------------- /media_tree/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | _('Media_tree') 3 | _('Media_Tree') 4 | -------------------------------------------------------------------------------- /media_tree/templates/media_tree/filenode/includes/figure.html: -------------------------------------------------------------------------------- 1 | {% extends "media_tree/filenode/includes/figure_base.html" %} -------------------------------------------------------------------------------- /media_tree/static/media_tree/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | jQuery = $.noConflict(true); 3 | })(django.jQuery); 4 | -------------------------------------------------------------------------------- /docs/_static/admin-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/docs/_static/admin-screenshot.png -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/preview.html: -------------------------------------------------------------------------------- 1 | {% include "media_tree/filenode/includes/preview.html" %} 2 | -------------------------------------------------------------------------------- /demo_project/media/upload/River.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/demo_project/media/upload/River.jpg -------------------------------------------------------------------------------- /demo_project/media/upload/Sunset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/demo_project/media/upload/Sunset.jpg -------------------------------------------------------------------------------- /demo_project/media/upload/Building.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/demo_project/media/upload/Building.jpg -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | Admin interface overview 2 | ************************ 3 | 4 | .. autoclass:: media_tree.admin.filenode_admin.FileNodeAdmin 5 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | FileNode Utility functions 2 | ************************** 3 | 4 | .. automodule:: media_tree.utils.filenode 5 | :members: 6 | -------------------------------------------------------------------------------- /demo_project/media/upload/Coconut_Trees.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/demo_project/media/upload/Coconut_Trees.jpg -------------------------------------------------------------------------------- /media_tree/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /media_tree/locale/de/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/de/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /media_tree/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /media_tree/locale/en/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/en/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /media_tree/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /media_tree/locale/nb/LC_MESSAGES/djangojs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/locale/nb/LC_MESSAGES/djangojs.mo -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/copy.png -------------------------------------------------------------------------------- /demo_project/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.6.2 2 | Pillow==2.3.1 3 | South==0.8.4 4 | django-mptt==0.6.0 5 | easy-thumbnails==1.5 6 | wsgiref==0.1.2 7 | django-media-tree>=0.8 8 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/drag-handler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/drag-handler.png -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/folder-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/folder-toggle.png -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/mimetypes/_blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/mimetypes/_blank.png -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/mimetypes/_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/mimetypes/_folder.png -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/folder-toggle-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/folder-toggle-loading.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /.coverage 3 | /.tox/ 4 | /build/ 5 | /dist/ 6 | /django-media-tree.egg/ 7 | /django_media_tree.egg-info/ 8 | /_thumbs/ 9 | *.py[co] 10 | _thumbs 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/img/icons/mimetypes/_folder_expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/img/icons/mimetypes/_folder_expanded.png -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/edit.gif -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/loading.gif -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/processing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/processing.gif -------------------------------------------------------------------------------- /media_tree/media_types.py: -------------------------------------------------------------------------------- 1 | FOLDER = 100 2 | FILE = 200 3 | ARCHIVE = 210 4 | AUDIO = 220 5 | DOCUMENT = 230 6 | IMAGE = 240 7 | SUPPORTED_IMAGE = 241 8 | VECTOR_IMAGE = 242 9 | TEXT = 250 10 | VIDEO = 260 11 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/placeholders/waiting-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/placeholders/waiting-generic.png -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/focal_point/static/focal_point/img/focal_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/contrib/media_extensions/images/focal_point/static/focal_point/img/focal_point.png -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/templates/cms/plugins/media_tree_image.html: -------------------------------------------------------------------------------- 1 | {% if not plugin.text_enabled %}

{% endif %} 2 | {% include "media_tree/filenode/includes/figure.html" %} 3 | {% if not plugin.text_enabled %}

{% endif %} 4 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/placeholders/not_available-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samluescher/django-media-tree/HEAD/media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/placeholders/not_available-generic.png -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/flat_change_list_results.html: -------------------------------------------------------------------------------- 1 |
2 | {% for result in results %} 3 |

4 | {{ result|safe }} 5 |

6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | Models and Managers 2 | ******************* 3 | 4 | .. automodule:: media_tree.models 5 | :members: 6 | :inherited-members: 7 | :exclude-members: clean, clean_fields, full_clean, save_base, 8 | serializable_value, validate_unique 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | AUTHORS AND MAINTAINERS 2 | 3 | MAIN DEVELOPERS: 4 | Samuel Luescher 5 | 6 | CONTRIBUTORS (in chronological order): 7 | hpoul (Herbert Poul) 8 | erleddalen (Erlend Dalen) 9 | mrhanky17 10 | bittner (Peter Bittner) 11 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_listing/templates/cms/plugins/media_tree_listing.html: -------------------------------------------------------------------------------- 1 | {% load media_tree_tags %} 2 | 5 | -------------------------------------------------------------------------------- /media_tree/contrib/views/templates/media_tree/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | {% block content %}{% endblock %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/iframe.xss.response-4.4.0.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | var match = /(\{.*\})/.exec(document.body.innerHTML); 4 | if (match) { 5 | parent.postMessage(match[1], "*"); 6 | } 7 | }()); 8 | -------------------------------------------------------------------------------- /demo_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /media_tree/contrib/views/templates/media_tree/filenode_list.html: -------------------------------------------------------------------------------- 1 | {% extends "media_tree/base.html" %} 2 | {% block content %} 3 | {% load media_tree_tags %} 4 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /media_tree/contrib/views/templates/media_tree/filenode_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "media_tree/base.html" %} 2 | {% block content %} 3 | {% load media_tree_tags %} 4 |

{{ object.title }}

5 | {% if object.description %}

{{ object.description }}

{% endif %} 6 |

{{ object|file_link:"include_icon include_size include_extension" }}

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | {% if not node %} 6 | {{ block.super }} 7 | {% else %} 8 | {% include "admin/media_tree/filenode/includes/breadcrumbs.html" %} 9 | {% endif %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /media_tree/templates/media_tree/filenode/includes/preview.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% if preview_file %} 3 | {% if not preview_file.is_icon %} 4 | {% include "media_tree/filenode/includes/thumbnail.html" %} 5 | {% else %} 6 | {% include "media_tree/filenode/includes/icon.html" %} 7 | {% endif %} 8 | {% endif %} 9 | {% endspaceless %} 10 | -------------------------------------------------------------------------------- /IDEAS.txt: -------------------------------------------------------------------------------- 1 | TODO: Add support for default templates (for instance 2 | "templates/media_types/image.html", "templates/extensions/flv.html") 3 | 4 | TODO: support for more file alternatives (preview, other streaming format etc) 5 | -- solution would probably be ManyToManyField to self with specifying a type. 6 | There would be a third node_type PACKAGE, a special kind of folder. 7 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/urls.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_image.views import ImagePluginDetailView 2 | from django.conf.urls.defaults import * 3 | 4 | urlpatterns = patterns('', 5 | url(r'^(?P\d+)/$', ImagePluginDetailView.as_view(), 6 | name='media_tree.contrib.cms_plugins.media_tree_image.ImagePluginDetailView'), 7 | ) 8 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/widget_preview.html: -------------------------------------------------------------------------------- 1 | {% spaceless %}{% load i18n %} 2 | 3 | {% if node %}{% include "admin/media_tree/filenode/includes/preview.html" %}{% else %}{% endif %} 4 | {% if node %}{{ node }}{% endif %} 5 | 6 | {% endspaceless %} 7 | -------------------------------------------------------------------------------- /media_tree/contrib/views/templates/media_tree/image_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "media_tree/base.html" %} 2 | {% block content %} 3 | {% include "media_tree/filenode/includes/figure.html" %} 4 | {% if link %} 5 |

6 | {{ link.text }}{% endif %} 7 |

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /media_tree/contrib/media_tree_modeltranslation/translation.py: -------------------------------------------------------------------------------- 1 | from modeltranslation.translator import translator, TranslationOptions 2 | from media_tree.models import FileNode 3 | 4 | class FileNodeTranslationOptions(TranslationOptions): 5 | fields = ('title', 'description', 'author', 'copyright', 'keywords', 'override_alt', 'override_caption') 6 | 7 | translator.register(FileNode, FileNodeTranslationOptions) 8 | 9 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/cms_app.py: -------------------------------------------------------------------------------- 1 | from cms.app_base import CMSApp 2 | from cms.apphook_pool import apphook_pool 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | class MediaTreeImagePluginApphook(CMSApp): 6 | name = _("Media Tree Image Detail") 7 | urls = ["media_tree.contrib.cms_plugins.media_tree_image.urls"] 8 | 9 | apphook_pool.register(MediaTreeImagePluginApphook) 10 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/focal_point/static/focal_point/css/focal_point.css: -------------------------------------------------------------------------------- 1 | .focal-point-container { 2 | position: absolute; 3 | top: -1px; 4 | left: -1px; 5 | z-index: 888; 6 | } 7 | 8 | .focal-point { 9 | background: url(../img/focal_point.png) no-repeat; 10 | width: 22px; 11 | height: 22px; 12 | position: absolute; 13 | cursor: pointer; 14 | z-index: 999; 15 | } 16 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/list_type_controls.html: -------------------------------------------------------------------------------- 1 | {% load i18n media_tree_admin %} 2 | {% trans "Tree" %} 3 | {% trans "Stream" %} 4 | -------------------------------------------------------------------------------- /media_tree/templates/media_tree/filenode/includes/icon.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% if preview_file %} 3 | 4 | {{ preview_file.alt }} 5 | 6 | {% endif %} 7 | {% endspaceless %} 8 | -------------------------------------------------------------------------------- /media_tree/contrib/legacy_mptt_support/templates/admin/mptt_change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load admin_list i18n mptt_admin %} 3 | 4 | {% block result_list %} 5 | {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} 6 | {% mptt_result_list cl %} 7 | {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/thumbnail_size_controls.html: -------------------------------------------------------------------------------- 1 | {% load i18n media_tree_admin %} 2 | {% for key, size in thumbnail_sizes.items %} 3 | {{ key }} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /demo_project/demo_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_project.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /docs/dummy/media_backends.py: -------------------------------------------------------------------------------- 1 | from media_tree.media_backends import MediaBackend 2 | from media_tree import media_types 3 | 4 | 5 | class DummyBackend(MediaBackend): 6 | 7 | SUPPORTED_MEDIA_TYPES = (media_types.SUPPORTED_IMAGE,) 8 | 9 | @staticmethod 10 | def check_conf(): 11 | pass 12 | 13 | @staticmethod 14 | def get_thumbnail(source, options): 15 | pass 16 | 17 | @staticmethod 18 | def get_valid_thumbnail_options(): 19 | return () -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/zipfiles/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | zipfiles 3 | ======== 4 | 5 | The *zipfiles* extension adds support for ZIP archives to the ``FileNodeAdmin``. 6 | If it is installed, you can select files and folders in the admin and 7 | download them as a ZIP archive. 8 | 9 | To install it, add the extension module to your ``INSTALLED_APPS`` setting:: 10 | 11 | INSTALLED_APPS = ( 12 | # ... your apps here ... 13 | 'media_tree.contrib.media_extensions.zipfiles' 14 | ) 15 | 16 | """ 17 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/upload_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | {% include "admin/media_tree/filenode/includes/breadcrumbs.html" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
{% csrf_token %} 10 |

{% trans "Upload file" %}

11 | {{ form.errors.as_p }} 12 | {{ form.as_p }} 13 |

14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /docs/bundled_extensions.rst: -------------------------------------------------------------------------------- 1 | .. _bundled-extensions: 2 | 3 | Bundled extensions 4 | ****************** 5 | 6 | Media Tree contains a few useful extensions in its ``contrib`` module. Since 7 | some of these extensions modify the ``FileNode`` model, you should install them 8 | before you run ``syncdb`` for the first time. 9 | 10 | .. automodule:: media_tree.contrib.media_extensions.images.focal_point 11 | :members: 12 | :inherited-members: 13 | 14 | .. automodule:: media_tree.contrib.media_extensions.zipfiles 15 | :members: 16 | :inherited-members: 17 | -------------------------------------------------------------------------------- /media_tree/templates/media_tree/filenode/includes/thumbnail.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% load media_tree_thumbnail %} 3 | {% if not thumbnail_size %} 4 | {% thumbnail_size as thumbnail_size %} 5 | {% endif %} 6 | {% if thumb or preview_file %} 7 | 8 | {% if not thumb %}{% thumbnail preview_file thumbnail_size as thumb %}{% endif %} 9 | {{ node.alt }} 10 | 11 | {% endif %} 12 | {% endspaceless %} 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | script: 3 | - python setup.py -q test 4 | env: # generate list with: $ tox -l | xargs -I ITEM echo " - TOXENV="ITEM 5 | - TOXENV=py26-django15 6 | - TOXENV=py26-django16 7 | - TOXENV=py27-django15 8 | - TOXENV=py27-django16 9 | - TOXENV=py27-django17 10 | - TOXENV=py27-django18 11 | - TOXENV=py32-django15 12 | - TOXENV=py32-django16 13 | - TOXENV=py32-django17 14 | - TOXENV=py32-django18 15 | - TOXENV=py33-django15 16 | - TOXENV=py33-django16 17 | - TOXENV=py33-django17 18 | - TOXENV=py33-django18 19 | - TOXENV=py34-django17 20 | - TOXENV=py34-django18 21 | -------------------------------------------------------------------------------- /media_tree/tests/__init__,py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/js/filenode_foreignkeywidget.js: -------------------------------------------------------------------------------- 1 | django.jQuery(function($) { 2 | $('.FileNodeForeignKeyRawIdWidget').delegate('.preview, .name', 'click', function() { 3 | $(this).closest('.FileNodeForeignKeyRawIdWidget').find('.related-lookup').trigger('click'); 4 | }); 5 | $('.FileNodeForeignKeyRawIdWidget').delegate('.clear-widget', 'click', function() { 6 | var $widget = $(this).closest('.FileNodeForeignKeyRawIdWidget'); 7 | $widget.find('input').val(''); 8 | $widget.find('.preview').empty(); 9 | $widget.find('.name').empty(); 10 | $(this).hide(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/zipfiles/zip_operations.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import os 3 | 4 | def write_nodes_recursive(archive, nodes, parent_path=''): 5 | for node in nodes: 6 | arcname = os.path.join(parent_path, node.name) 7 | if node.is_file(): 8 | archive.write(node.file.path, arcname) 9 | elif node.is_folder(): 10 | write_nodes_recursive(archive, node.get_children(), arcname) 11 | 12 | def compress_nodes(file, nodes): 13 | archive = zipfile.ZipFile(file, "w", zipfile.ZIP_DEFLATED) 14 | write_nodes_recursive(archive, nodes) 15 | archive.close() 16 | return archive 17 | 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | include CHANGELOG.txt 5 | recursive-include media_tree/locale * 6 | recursive-include media_tree/static * 7 | recursive-include media_tree/contrib * 8 | recursive-include media_tree/templates * 9 | include demo_project/requirements.txt 10 | recursive-include demo_project/manage.py * 11 | recursive-include demo_project/demo_project * 12 | recursive-include demo_project/media * 13 | recursive-exclude demo_project/media/upload/_thumbs * 14 | recursive-include demo_project/templates * 15 | recursive-include demo_project/fixtures * 16 | recursive-include docs * 17 | recursive-exclude * *.pyc -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The module ``media_tree.contrib.cms_plugins`` contains a number of plugins for 3 | using ``FileNode`` objects on pages created with 4 | `Django CMS `_. 5 | 6 | Installation 7 | ============ 8 | 9 | For optimum admin functionality when using these plugins, you should put 10 | ``media_tree.contrib.cms_plugins`` in your installed apps, and run 11 | ``manage.py collectstatic``. 12 | 13 | If you are not using the ``staticfiles`` app, you have to manually copy 14 | the contents of the ``static`` folder to your static root. 15 | 16 | """ 17 | 18 | # TODO: Needs to honor published=False, also of subfolders. -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/settings.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.helpers import PluginLink 2 | from django.utils.translation import ugettext_lazy as _ 3 | from django.conf import settings 4 | 5 | MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_CHOICES = ( 6 | (PluginLink.LINK_PAGE, _('Link to page')), 7 | (PluginLink.LINK_URL, _('Link to web address')), 8 | (PluginLink.LINK_IMAGE_DETAIL, _('Link to image detail')), 9 | (PluginLink.LINK_URL_REVERSE, _('Link to URL pattern')), 10 | ) 11 | 12 | MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_DEFAULT = PluginLink.LINK_IMAGE_DETAIL \ 13 | if getattr(settings, 'CMS_APPLICATIONS_URLS', {}).has_key('media_tree.urls') else None 14 | 15 | -------------------------------------------------------------------------------- /media_tree/contrib/legacy_mptt_support/templatetags/media_tree_admin.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.admin.templatetags.admin_list import result_headers, results 3 | 4 | register = template.Library() 5 | 6 | # from django.contrib.admin.templatetags.admin_list.result_list(cl) 7 | def _result_list(cl): 8 | return {'cl': cl, 9 | 'result_headers': list(result_headers(cl)), 10 | 'results': list(cl.result_list) #list(results(cl)) 11 | } 12 | 13 | def result_list_thumbnails(cl): 14 | return _result_list(cl) 15 | result_list_thumbnails = register.inclusion_tag("admin/media_tree/filenode/change_list_results_thumbnails.html")(result_list_thumbnails) 16 | -------------------------------------------------------------------------------- /media_tree/extension/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree.extension.base_extenders import MediaTreeExtender 2 | from media_tree.extension.base_extenders.admin_extender import AdminExtender 3 | from media_tree.extension.base_extenders.form_extender import FormExtender 4 | from media_tree.extension.base_extenders.model_extender import ModelExtender 5 | 6 | 7 | def register(extender): 8 | """Registers the `extender` class and lets it contribute its members to 9 | the respective extended classes during runtime. 10 | """ 11 | if not issubclass(extender, MediaTreeExtender): 12 | raise NotImplementedError('Class `%s` needs to be a subclass of `MediaTreeExtender`.' % extender) 13 | extender.contribute() 14 | 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{26,27,32,33}-django{15,16}, 4 | py{27,32,33,34}-django{17,18}, 5 | 6 | [testenv] 7 | changedir = demo_project 8 | commands = 9 | python manage.py syncdb --noinput 10 | python manage.py validate 11 | # TODO: for Django>=1.7 execute instead: === 12 | # python manage.py migrate --noinput 13 | # python manage.py check 14 | python manage.py loaddata initial_data 15 | deps = 16 | # Pillow 17 | django-mptt 18 | easy-thumbnails 19 | # wsgiref 20 | django15,django16: South 21 | django15: Django>=1.5,<1.6 22 | django16: Django>=1.6,<1.7 23 | django17: Django>=1.7,<1.8 24 | django18: Django>=1.8,<1.9 25 | passenv = 26 | TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 27 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin: Image 3 | ************* 4 | 5 | This plugin allows you to put a single picture on a page, as a figure complete 6 | with caption and other metadata. 7 | 8 | Installation 9 | ============ 10 | 11 | To use this plugin, put ``media_tree.contrib.cms_plugins.media_tree_image`` 12 | in your installed apps, and run ``manage.py syncdb``. 13 | 14 | Template 15 | ======== 16 | 17 | Override the template ``cms/plugins/media_tree_image.html`` if you want to 18 | customize the output. Please take a look at the default template for more 19 | information. 20 | 21 | By default, images are rendered to the output using the template 22 | ``media_tree/filenode/includes/figure.html``, which includes captions. 23 | """ -------------------------------------------------------------------------------- /docs/management.rst: -------------------------------------------------------------------------------- 1 | Management Commands 2 | ******************* 3 | 4 | You can use the following management commands to assist you with media file 5 | management. 6 | 7 | 8 | Orphaned files 9 | ============== 10 | 11 | Use the following command to list all orphaned files, i.e. media files existing 12 | in storage that are not in the database:: 13 | 14 | manage.py mediaorphaned 15 | 16 | Use the following command to **delete** all orphaned files:: 17 | 18 | manage.py mediaorphaned --delete 19 | 20 | 21 | Media cache 22 | =========== 23 | 24 | Use the following command to list all media cache files, such as thumbnails:: 25 | 26 | manage.py mediacache 27 | 28 | Use the following command to **delete** all media cache files:: 29 | 30 | manage.py mediacache --delete 31 | -------------------------------------------------------------------------------- /media_tree/contrib/views/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The module ``media_tree.contrib.views`` contains class-based generic views that 3 | enable you to access ``FileNode`` objects through public URLs. Please see below 4 | for specific examples. Of course you can also extend the generic view classes to 5 | create views that suit your specific requirements. 6 | 7 | .. Note:: 8 | As with any public views, you may want to restrict the objects that should be 9 | publicly visible by passing an appropriately filtered ``queryset`` when 10 | implementing a view. For instance, you may not want users to see the internal 11 | folder structure of your ``FileNode`` objects, hence using a ``FileNodeListingView`` 12 | with a QuerySet such as ``FileNode.objects.all()`` would be a bad idea. 13 | """ -------------------------------------------------------------------------------- /docs/bundled_plugins.rst: -------------------------------------------------------------------------------- 1 | Django CMS Plugins 2 | ****************** 3 | 4 | .. automodule:: media_tree.contrib.cms_plugins 5 | :members: 6 | 7 | .. Note:: 8 | Of course you can also create your own models and plugins using ``FileNode`` 9 | objects. Please take a look at :ref:`fields` and :ref:`custom-plugins-howto` 10 | for more information on how to integrate Media Tree with your own applications. 11 | 12 | .. automodule:: media_tree.contrib.cms_plugins.media_tree_listing 13 | :members: 14 | 15 | .. automodule:: media_tree.contrib.cms_plugins.media_tree_image 16 | :members: 17 | 18 | .. automodule:: media_tree.contrib.cms_plugins.media_tree_slideshow 19 | :members: 20 | 21 | .. automodule:: media_tree.contrib.cms_plugins.media_tree_gallery 22 | :members: 23 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 23 | -------------------------------------------------------------------------------- /docs/views.rst: -------------------------------------------------------------------------------- 1 | .. _generic-views: 2 | 3 | Class-based generic views 4 | ************************* 5 | 6 | .. automodule:: media_tree.contrib.views 7 | :members: 8 | 9 | List Views 10 | ========== 11 | 12 | .. autoclass:: media_tree.contrib.views.listing.FileNodeListingView 13 | :members: 14 | :inherited-members: 15 | 16 | .. autoclass:: media_tree.contrib.views.listing.FileNodeListingFilteredByFolderView 17 | :members: 18 | :inherited-members: 19 | 20 | Detail Views 21 | ============ 22 | 23 | .. autoclass:: media_tree.contrib.views.detail.FileNodeDetailView 24 | :members: 25 | :inherited-members: 26 | :exclude-members: get_slug_field 27 | 28 | .. autoclass:: media_tree.contrib.views.detail.image.ImageNodeDetailView 29 | :members: 30 | :inherited-members: 31 | :exclude-members: get_slug_field 32 | 33 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_listing/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin: File listing 3 | ******************** 4 | 5 | This plugin allows you to put a file listing on a page, displaying download 6 | links for the selected ``FileNode`` objects in a folder tree. 7 | 8 | The folder tree that is rendered does not have to be identical to the actual tree 9 | in your media library. Instead, you can group arbitrary nodes, or output a merged 10 | (flat) list. 11 | 12 | Installation 13 | ============ 14 | 15 | To use this plugin, put ``media_tree.contrib.cms_plugins.media_tree_listing`` 16 | in your installed apps, and run ``manage.py syncdb``. 17 | 18 | Template 19 | ======== 20 | 21 | Override the template ``cms/plugins/media_tree_listing.html`` if you want to 22 | customize the output. Please take a look at the default template for more 23 | information. 24 | """ -------------------------------------------------------------------------------- /media_tree/static/media_tree/css/filenode_preview.css: -------------------------------------------------------------------------------- 1 | /* Thumbnails */ 2 | 3 | #changelist .thumbnail img, #ghost .thumbnail img, .change-form .thumbnail img, .FileNodeForeignKeyRawIdWidget .thumbnail img { 4 | display: inline-block; 5 | border: 1px solid #ccc; 6 | padding: 3px; 7 | background: white; 8 | border-radius: 3px; 9 | line-height: 1px; 10 | -moz-box-shadow: 0px 3px 7px rgba(0, 0, 0, .1); 11 | -webkit-box-shadow: 0px 3px 7px rgba(0, 0, 0, .1); 12 | box-shadow: 0px 3px 7px rgba(0, 0, 0, .1); 13 | } 14 | 15 | .FileNodeForeignKeyRawIdWidget .preview { 16 | display: inline-block; 17 | margin: .5em 1em .5em 1em; 18 | } 19 | 20 | .FileNodeForeignKeyRawIdWidget .preview, .FileNodeForeignKeyRawIdWidget .name { 21 | cursor: pointer; 22 | } 23 | 24 | .FileNodeForeignKeyRawIdWidget .clear-widget { 25 | margin-left: .5em; 26 | } 27 | -------------------------------------------------------------------------------- /media_tree/contrib/legacy_mptt_support/templates/admin/mptt_change_list_results.html: -------------------------------------------------------------------------------- 1 | {% if result_hidden_fields %} 2 |
{# DIV for HTML validation #} 3 | {% for item in result_hidden_fields %}{{ item }}{% endfor %} 4 |
5 | {% endif %} 6 | {% if results %} 7 |
8 | 9 | 10 | 11 | {% for header in result_headers %}{% endfor %} 14 | 15 | 16 | 17 | {% for result in results %} 18 | {% if result.form.non_field_errors %} 19 | 20 | {% endif %} 21 | {% for item in result %}{{ item }}{% endfor %} 22 | {% endfor %} 23 | 24 |
12 | {{ header.text|capfirst }} 13 |
{{ result.form.non_field_errors }}
25 |
26 | {% endif %} -------------------------------------------------------------------------------- /media_tree/migrations/0003_mptt_to_treebeard_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | def increment_depth(apps, schema_editor): 8 | FileNode = apps.get_model("media_tree", "FileNode") 9 | for node in FileNode.objects.all(): 10 | # Treebeard root nodes have a depth of 1 11 | node.depth += 1 12 | node.save() 13 | 14 | def decrement_depth(apps, schema_editor): 15 | FileNode = apps.get_model("media_tree", "FileNode") 16 | for node in FileNode.objects.all(): 17 | # MPTT root nodes have a depth of 0 18 | node.depth -= 1 19 | node.save() 20 | 21 | 22 | class Migration(migrations.Migration): 23 | 24 | dependencies = [ 25 | ('media_tree', '0002_mptt_to_treebeard'), 26 | ] 27 | 28 | operations = [ 29 | migrations.RunPython(increment_depth, decrement_depth), 30 | ] 31 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_slideshow/settings.py: -------------------------------------------------------------------------------- 1 | MEDIA_TREE_SLIDESHOW_TRANSITION_FX_CHOICES = ( 2 | ('blindX', 'blindX',), 3 | ('blindY', 'blindY',), 4 | ('blindZ', 'blindZ',), 5 | ('cover', 'cover',), 6 | ('curtainX', 'curtainX',), 7 | ('curtainY', 'curtainY',), 8 | ('fade', 'fade',), 9 | ('fadeZoom', 'fadeZoom',), 10 | ('growX', 'growX',), 11 | ('growY', 'growY',), 12 | ('none', 'none',), 13 | ('scrollUp', 'scrollUp',), 14 | ('scrollDown', 'scrollDown',), 15 | ('scrollLeft', 'scrollLeft',), 16 | ('scrollRight', 'scrollRight',), 17 | ('scrollHorz', 'scrollHorz',), 18 | ('scrollVert', 'scrollVert',), 19 | ('shuffle', 'shuffle',), 20 | ('slideX', 'slideX',), 21 | ('slideY', 'slideY',), 22 | ('toss', 'toss',), 23 | ('turnUp', 'turnUp',), 24 | ('turnDown', 'turnDown',), 25 | ('turnLeft', 'turnLeft',), 26 | ('turnRight', 'turnRight',), 27 | ('uncover', 'uncover',), 28 | ('wipe', 'wipe',), 29 | ('zoom', 'zoom',), 30 | ) -------------------------------------------------------------------------------- /media_tree/contrib/media_backends/sorl_thumbnail/__init__.py: -------------------------------------------------------------------------------- 1 | raise NotImplementedError('SorlThumbnailBackend is experimental and currently not officially supported') 2 | 3 | from media_tree.media_backends import MediaBackend, ThumbnailError 4 | from media_tree import media_types 5 | from sorl.thumbnail import get_thumbnail 6 | 7 | class SorlThumbnailBackend(MediaBackend): 8 | """ 9 | Media backend for sorl.thumbnails support. 10 | Experimental and currently not officially supported. 11 | """ 12 | 13 | SUPPORTED_MEDIA_TYPES = (media_types.SUPPORTED_IMAGE,) 14 | 15 | @staticmethod 16 | def get_thumbnail(source, options): 17 | size = options['size'] 18 | del options['size'] 19 | if not isinstance(size, basestring): 20 | size = 'x'.join([str(s) for s in size]) 21 | try: 22 | thumbnail = get_thumbnail(source, size, **options) 23 | except Exception as inst: 24 | raise ThumbnailError(inst) 25 | return thumbnail 26 | 27 | -------------------------------------------------------------------------------- /media_tree/management/commands/mediacache.py: -------------------------------------------------------------------------------- 1 | from media_tree.utils.maintenance import get_cache_files 2 | from media_tree.utils import get_media_storage 3 | from django.core.management.base import BaseCommand, CommandError 4 | from optparse import make_option 5 | 6 | class Command(BaseCommand): 7 | 8 | help = 'Lists (and optionally deletes) all media_tree cache files.' 9 | 10 | option_list = BaseCommand.option_list + ( 11 | make_option('--delete', 12 | action='store_true', 13 | dest='delete', 14 | default=False, 15 | help='Delete all cache files'), 16 | ) 17 | 18 | def handle(self, *args, **options): 19 | cache_files = get_cache_files() 20 | storage = get_media_storage() 21 | for path in cache_files: 22 | if options['delete']: 23 | storage.delete(path) 24 | self.stdout.write("Deleted %s\n" % storage.path(path)) 25 | else: 26 | self.stdout.write("%s\n" % storage.path(path)) 27 | -------------------------------------------------------------------------------- /media_tree/contrib/media_backends/vector_image/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree.media_backends import MediaBackend 2 | from media_tree import media_types 3 | from django.db.models.fields.files import FieldFile 4 | 5 | 6 | class VectorThumbnail(FieldFile): 7 | def __init__(self, *args, **kwargs): 8 | super(VectorThumbnail, self).__init__(*args, **kwargs) 9 | self.width = None 10 | self.height = None 11 | 12 | 13 | class VectorImageBackend(MediaBackend): 14 | """ 15 | Media backend for browser-supported vector files. Returns no real 16 | thumbnail, but a wrapper to the original vector image. 17 | """ 18 | 19 | SUPPORTED_MEDIA_TYPES = (media_types.VECTOR_IMAGE,) 20 | 21 | @staticmethod 22 | def supports_thumbnails(): 23 | return True 24 | 25 | @staticmethod 26 | def get_thumbnail(source, options): 27 | thumb = VectorThumbnail(None, source, source.name) 28 | thumb.width = options['size'][0] 29 | thumb.height = options['size'][1] 30 | return thumb 31 | 32 | @staticmethod 33 | def get_valid_thumbnail_options(): 34 | return () 35 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_slideshow/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin: Slideshow 3 | ***************** 4 | 5 | This plugin allows you to put a slideshow on a page, automatically 6 | displaying the selected image files with customizable transitions and 7 | intervals. 8 | 9 | 10 | Installation 11 | ============ 12 | 13 | To use this plugin, put ``media_tree.contrib.cms_plugins.media_tree_slideshow`` 14 | in your installed apps, and run ``manage.py syncdb``. 15 | 16 | 17 | Template 18 | ======== 19 | 20 | Override the template ``cms/plugins/media_tree_slideshow.html`` if you want to 21 | customize the output. Please take a look at the default template for more 22 | information. 23 | 24 | By default, images are rendered to the output using the template 25 | ``media_tree/filenode/includes/figure.html``, which includes captions. 26 | 27 | .. Note:: 28 | The default template requires you to include `jQuery `_ 29 | in your pages, since it uses the `jQuery Cycle Plugin 30 | `_ (bundled) for image transitions. 31 | """ -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_gallery/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin: Gallery 3 | *************** 4 | 5 | This plugin allows you to put an image gallery on a page. Galleries can include 6 | nested folder structures or display merged (flat) compositions of all images in 7 | a range of subfolders. Pictures can be browsed or auto-played. 8 | 9 | Installation 10 | ============ 11 | 12 | To use this plugin, put ``media_tree.contrib.cms_plugins.media_tree_gallery`` 13 | in your installed apps, and run ``manage.py syncdb``. 14 | 15 | Template 16 | ======== 17 | 18 | Override the template ``cms/plugins/media_tree_gallery.html`` if you want to 19 | customize the output. Please take a look at the default template for more 20 | information. 21 | 22 | By default, images are rendered to the output using the template 23 | ``media_tree/filenode/includes/figure.html``, which includes captions. 24 | 25 | .. Note:: 26 | The default template requires you to include `jQuery `_ 27 | in your pages, since it uses the `jQuery Cycle Plugin 28 | `_ (bundled) for image transitions. 29 | """ -------------------------------------------------------------------------------- /media_tree/management/commands/mediaorphaned.py: -------------------------------------------------------------------------------- 1 | from media_tree.utils.maintenance import get_orphaned_files 2 | from media_tree.utils import get_media_storage 3 | from django.core.management.base import BaseCommand, CommandError 4 | from optparse import make_option 5 | 6 | class Command(BaseCommand): 7 | 8 | help = 'Lists (and optionally deletes) all media_tree orphaned files, ' \ 9 | + 'i.e. media files existing in storage that are not in the database.' 10 | 11 | option_list = BaseCommand.option_list + ( 12 | make_option('--delete', 13 | action='store_true', 14 | dest='delete', 15 | default=False, 16 | help='Delete all orphaned files from storage'), 17 | ) 18 | 19 | def handle(self, *args, **options): 20 | orphaned_files = get_orphaned_files() 21 | storage = get_media_storage() 22 | for path in orphaned_files: 23 | if options['delete']: 24 | storage.delete(path) 25 | self.stdout.write("Deleted %s\n" % storage.path(path)) 26 | else: 27 | self.stdout.write("%s\n" % storage.path(path)) 28 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_gallery/models.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_listing.models import MediaTreeListingBase 2 | from media_tree.contrib.cms_plugins.media_tree_slideshow.models import MediaTreeImageListingBase, MediaTreeImageItemBase 3 | from media_tree.fields import FileNodeForeignKey 4 | from media_tree.contrib.views.listing import LISTING_MERGED, LISTING_NESTED 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | class MediaTreeGallery(MediaTreeImageListingBase): 10 | list_type = models.CharField(_('gallery type'), max_length=1, default=LISTING_MERGED, 11 | choices=((LISTING_MERGED, _('merged')), (LISTING_NESTED, _('nested'))), 12 | help_text=_('A nested gallery includes a browseable folder list. A merged gallery displays media from all folders merged into a flat list.')) 13 | auto_play = models.BooleanField('auto play', default=False) 14 | 15 | 16 | class MediaTreeGalleryItem(MediaTreeImageItemBase): 17 | list_plugin = models.ForeignKey(MediaTreeGallery, related_name='media_items') 18 | node = FileNodeForeignKey(verbose_name=_('folder/file')) 19 | -------------------------------------------------------------------------------- /media_tree/admin/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.views.main import SEARCH_VAR 2 | 3 | try: 4 | from threading import local 5 | except ImportError: 6 | from django.utils._threading_local import local 7 | 8 | _thread_locals = local() 9 | 10 | def set_current_request(request): 11 | _thread_locals.request = request 12 | 13 | def get_current_request(): 14 | """ returns the request object for this thread """ 15 | return getattr(_thread_locals, "request", None) 16 | 17 | # Sometimes we need to pass around parameters between standard ModelAdmin methods, 18 | # and since the methods don't have these parameters, we are passing them through a 19 | # dictionary in the request object. This is hackish, but there currently is no 20 | # better solution. 21 | def set_request_attr(request, attr, value): 22 | if not hasattr(request, 'media_tree'): 23 | request.media_tree = {} 24 | request.media_tree[attr] = value 25 | 26 | def get_request_attr(request, attr, default=None): 27 | if not hasattr(request, 'media_tree'): 28 | return default 29 | return request.media_tree.get(attr, default) 30 | 31 | def is_search_request(request): 32 | return request.GET.get(SEARCH_VAR, None) != None -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/focal_point/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | focal_point 3 | =========== 4 | 5 | The *focal_point* extension allows you to drag a marker on image thumbnails 6 | while editing, thus specifying the most relevant portion of the image. You can 7 | then use these coordinates in templates for image cropping. 8 | 9 | - To install it, add the extension module to your ``INSTALLED_APPS`` setting:: 10 | 11 | INSTALLED_APPS = ( 12 | # ... your apps here ... 13 | 'media_tree.contrib.media_extensions.images.focal_point' 14 | ) 15 | 16 | - If you are not using ``django.contrib.staticfiles``, copy the contents of the 17 | ``static`` folder to the static root of your project. If you are using the 18 | ``staticfiles`` app, just run the usual command to collect static files:: 19 | 20 | $ ./manage.py collectstatic 21 | 22 | .. Note:: 23 | This extension adds the fields ``focal_x`` and ``focal_y`` to 24 | the ``FileNode`` model. You are going to have to add these fields to 25 | the database table yourself by modifying the ``media_tree_filenode`` table 26 | with a database client, **unless you installed it before running** 27 | ``syncdb``). 28 | 29 | """ 30 | -------------------------------------------------------------------------------- /media_tree/extension/base_extenders/form_extender.py: -------------------------------------------------------------------------------- 1 | from media_tree.extension.base_extenders import MediaDefiningExtender 2 | from media_tree.admin.forms import FileForm 3 | 4 | 5 | class FormExtender(MediaDefiningExtender): 6 | """ 7 | In order to extend form classes, for instance to load additional form media, 8 | you need to subclass the this class and define the appropriate attributes: 9 | """ 10 | 11 | Media = None 12 | """A Media class, defined exactly like the Media classes of a Form or 13 | a ModelAdmin. The Media definitions are merged into the extended class. 14 | """ 15 | 16 | @classmethod 17 | def contribute(extender, extended_class=FileForm): 18 | super(FormExtender, extender).contribute(extended_class) 19 | 20 | # TODO: what about the `extend` property? Media extender should 21 | # be able to override media instead of extending. 22 | 23 | # Extends fieldsets with extender fieldsets 24 | if hasattr(extender.Meta, 'fieldsets'): 25 | if not getattr(extended_class.Meta, 'fieldsets', None): 26 | extended_class.Meta.fieldsets = [] 27 | extended_class.Meta.fieldsets.extend(extender.Meta.fieldsets) 28 | -------------------------------------------------------------------------------- /media_tree/admin/actions/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import helpers 2 | from media_tree.models import FileNode 3 | from django.utils.translation import ugettext as _ 4 | 5 | def get_actions_context(modeladmin): 6 | return { 7 | 'node': FileNode.get_top_node(), # TODO get current folder 8 | "opts": modeladmin.model._meta, 9 | #"root_path": modeladmin.admin_site.root_path, 10 | "app_label": modeladmin.model._meta.app_label, 11 | 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 12 | } 13 | 14 | def execute_empty_queryset_action(modeladmin, request): 15 | action = request.POST.get('action', None) 16 | if request.method == 'POST' and action and not request.POST.get(helpers.ACTION_CHECKBOX_NAME, None): 17 | actions = modeladmin.get_actions(request) 18 | if actions.has_key(action): 19 | func = actions[action][0] 20 | if getattr(actions[action][0], 'allow_empty_queryset', False): 21 | return func(modeladmin, request) 22 | else: 23 | pass 24 | # Django 1.2 already creates a message 25 | # request.user.message_set.create(message=_('No %s selected.') % FileNode._meta.verbose_name_plural) 26 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | .. _media-backends: 2 | 3 | Using FileNodes in templates 4 | **************************** 5 | 6 | Although Media Tree is designed to be agnostic of the module you use to generate 7 | image versions and thumbnails, it includes some tags to assist you with 8 | generating thumbnails from ``FileNode`` objects, since this is one of the most 9 | common tasks when working with image files in web applications. 10 | 11 | 12 | A word about Media Backends 13 | =========================== 14 | 15 | Media Tree's template tags do not use an imaging toolkit directly, but an 16 | abstraction class designed to wrap the actual image manipulation handled by a 17 | third-party module (such as ``easy_thumbnails`` or ``sorl.thumbnail``, to name 18 | two popular choices). 19 | 20 | The advantage of wrapping thumbnail generation like this is that Media Tree does 21 | not need to depend on a specific image generation library, with the additional 22 | benefit that you can just use the abstract template tags in your templates and 23 | switch to another ``MediaBackend`` at any time. 24 | 25 | 26 | Thumbnail Template Tags 27 | ======================= 28 | 29 | .. automodule:: media_tree.templatetags.media_tree_thumbnail 30 | :members: 31 | :exclude-members: split_args 32 | -------------------------------------------------------------------------------- /media_tree/templatetags/media_tree_admin.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.admin.templatetags.admin_list import items_for_result 3 | import re 4 | 5 | register = template.Library() 6 | 7 | TH_REGEX = re.compile(']+)?>(.*)') 8 | 9 | 10 | def th_for_result(cl, res): 11 | for item in items_for_result(cl, res, None): 12 | match = TH_REGEX.match(item) 13 | if match: 14 | return match.group(2) 15 | 16 | 17 | @register.inclusion_tag( 18 | 'admin/media_tree/filenode/flat_change_list_results.html', takes_context=True) 19 | def result_tree_flat(context, cl, request): 20 | """ 21 | Added 'filtered' param, so the template's js knows whether the results have 22 | been affected by a GET param or not. Only when the results are not filtered 23 | you can drag and sort the tree 24 | """ 25 | 26 | return { 27 | #'filtered': is_filtered_cl(cl, request), 28 | 'results': (th_for_result(cl, res) for res in list(cl.result_list)), 29 | } 30 | 31 | 32 | @register.simple_tag(takes_context=True) 33 | def query_string(context, *args, **kwargs): 34 | get = context['request'].GET.copy() 35 | get.update(kwargs) 36 | return '?' + '&'.join(["%s=%s" % (key, value) for key, value in get.items()]) 37 | -------------------------------------------------------------------------------- /media_tree/templatetags/media_tree_tags.py: -------------------------------------------------------------------------------- 1 | from media_tree.models import FileNode 2 | from media_tree.utils.filenode import get_file_link 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | def get_kwargs_for_file_link(opts): 8 | kwargs = { 9 | 'use_metadata': False, 10 | 'include_size': False, 11 | 'include_extension': False, 12 | 'include_icon': False 13 | } 14 | if isinstance(opts, basestring): 15 | for key in opts.split(' '): 16 | kwargs[key] = True 17 | elif isinstance(opts, dict): 18 | kwargs.update(opts) 19 | return kwargs 20 | 21 | def file_links(items, opts=None): 22 | """ 23 | Turns a (optionally nested) list of FileNode objects into a list of 24 | strings, linking to the associated files. 25 | """ 26 | result = [] 27 | kwargs = get_kwargs_for_file_link(opts) 28 | for item in items: 29 | if isinstance(item, FileNode): 30 | result.append(get_file_link(item, **kwargs)) 31 | else: 32 | result.append(file_links(item, kwargs)) 33 | return result 34 | 35 | register.filter(file_links) 36 | 37 | def file_link(node, opts=None): 38 | """ 39 | Turns a FileNode object into a string, linking to the associated file. 40 | """ 41 | kwargs = get_kwargs_for_file_link(opts) 42 | return get_file_link(node, **kwargs) 43 | 44 | register.filter(file_link) -------------------------------------------------------------------------------- /media_tree/templates/media_tree/filenode/includes/figure_base.html: -------------------------------------------------------------------------------- 1 | {% load media_tree_thumbnail %} 2 | 3 | {% spaceless %} 4 | {% if image_node.link %}{% endif %} 5 | {% block image %} 6 | {% if thumbnail_size %} 7 | {% thumbnail image_node thumbnail_size as thumb %} 8 | {{ image_node.alt }} 9 | {% else %} 10 | {{ image_node.alt }} 11 | {% endif %} 12 | {% endblock %} 13 | {% if image_node.link %}{% endif %} 14 | {% if thumbnail_size %} 15 | 16 | {% endif %} 17 | {% endspaceless %} 18 | {% if image_node.has_metadata %} 19 | {% block caption %} 20 | 21 | {{ image_node.get_caption_formatted }} 22 | 23 | {% endblock %} 24 | {% endif %} 25 | 26 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_slideshow/templates/cms/plugins/media_tree_slideshow.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | {% addtoblock "js" %}{% endaddtoblock %} 3 |
4 | {% for image_node in node_list %} 5 | {% include "cms/plugins/media_tree_image.html" %} 6 | {% endfor %} 7 |
8 | {% addtoblock "js" %} 9 | 32 | {% endaddtoblock %} 33 | -------------------------------------------------------------------------------- /media_tree/contrib/views/helpers.py: -------------------------------------------------------------------------------- 1 | from media_tree.utils.filenode import get_file_link 2 | 3 | class FolderLinkBase(object): 4 | 5 | filter_media_types = None 6 | selected_folder = None 7 | folder_param_name = None 8 | count_descendants = False 9 | count_children = False 10 | 11 | def __init__(self, node): 12 | self.node = node 13 | 14 | def get_link_content(self): 15 | return self.node.__unicode__() 16 | 17 | # TODO: This should include the default image for each folder as its icon 18 | def __unicode__(self): 19 | 20 | if self.count_descendants: 21 | count_qs = self.node.get_descendants() 22 | elif self.count_children: 23 | count_qs = self.node.get_children() 24 | else: 25 | count_qs = None 26 | 27 | if count_qs: 28 | if self.filter_media_types: 29 | count_qs = count_qs.filter(media_type__in=self.filter_media_types) 30 | count = ' (%i)' % count_qs.count() 31 | else: 32 | count = '' 33 | extra_class = '' 34 | if self.node == self.selected_folder: 35 | extra_class = 'selected' 36 | query = '?%s=%i' % (self.folder_param_name, self.node.pk) 37 | return get_file_link(self.node, href=query, extra=count, extra_class=extra_class, include_icon=True) 38 | -------------------------------------------------------------------------------- /demo_project/demo_project/urls.py: -------------------------------------------------------------------------------- 1 | from media_tree.models import FileNode 2 | from media_tree.contrib.views.listing import FileNodeListingView 3 | from media_tree.contrib.views.detail import FileNodeDetailView 4 | from media_tree.contrib.views.detail.image import ImageNodeDetailView 5 | from django.views.generic.base import TemplateView 6 | from django.conf.urls import patterns, include, url 7 | 8 | from django.contrib import admin 9 | admin.autodiscover() 10 | 11 | urlpatterns = patterns( 12 | '', 13 | (r'^$', TemplateView.as_view(template_name="media_tree/base.html")), 14 | 15 | url(r'^listing/$', FileNodeListingView.as_view( 16 | # notice that queryset can be any iterable, for instance a list: 17 | queryset=FileNode.objects.filter(lvl=0), 18 | ), name="demo_listing"), 19 | 20 | url(r'^files/(?P.+)/$', FileNodeDetailView.as_view( 21 | queryset=FileNode.objects.filter(extension='txt') 22 | ), name="demo_detail"), 23 | 24 | url(r'^images/(?P.+)/$', ImageNodeDetailView.as_view( 25 | queryset=FileNode.objects.get(path='Example Pictures').get_descendants() 26 | ), name="demo_image"), 27 | 28 | url(r'^admin/', include(admin.site.urls)), 29 | ) 30 | 31 | # do NOT use this on a production server 32 | from django.conf import settings 33 | from django.conf.urls.static import static 34 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 35 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/zipfiles/media_extension.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.media_extensions.zipfiles import zip_operations as operations 2 | from media_tree import extension 3 | from cStringIO import StringIO 4 | from django.http import HttpResponse 5 | from django.utils.translation import ugettext, ugettext_lazy as _ 6 | 7 | # TODO: Implement extract_selected_archives, which unzips the selected archives into 8 | # a selectable or new destination folder 9 | class ZipFileAdminExtender(extension.AdminExtender): 10 | 11 | def download_selected_as_archive(modeladmin, request, queryset): 12 | if queryset.count() == 1: 13 | file_name = queryset[0].name 14 | else: 15 | file_name = ugettext('Archive') 16 | file_ext = 'zip' 17 | 18 | response = HttpResponse(mimetype='application/zip') 19 | response['Content-Disposition'] = 'attachment; filename=%s.%s' % ( 20 | file_name, file_ext) 21 | buffer = StringIO() 22 | operations.compress_nodes(buffer, queryset) 23 | buffer.flush() 24 | response.write(buffer.getvalue()) 25 | response['Content-Length'] = len(response.content); 26 | buffer.close() 27 | return response 28 | download_selected_as_archive.short_description = _('Download selected %(verbose_name_plural)s as archive') 29 | 30 | actions = [download_selected_as_archive] 31 | 32 | extension.register(ZipFileAdminExtender) 33 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/includes/uploader_button_js.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 34 | -------------------------------------------------------------------------------- /media_tree/extension/base_extenders/admin_extender.py: -------------------------------------------------------------------------------- 1 | from media_tree.admin import FileNodeAdmin 2 | from media_tree.extension.base_extenders import MediaDefiningExtender 3 | 4 | 5 | class AdminExtender(MediaDefiningExtender): 6 | """In order to extend the ModelAdmin, you need to subclass this class and 7 | define the appropriate attributes: 8 | """ 9 | 10 | Media = None 11 | """A Media class, defined exactly like the Media classes of a Form or 12 | a ModelAdmin. The Media definitions are merged into the extended class. 13 | """ 14 | 15 | actions = None 16 | """A list of admin actions that are added to the ModelAdmin calling its 17 | method `register_action(func, required_perms)`. 18 | Each item in this list can either be a callable or a tuple containing the 19 | callable and a list of permissions required to execute an action. 20 | 21 | For example:: 22 | 23 | actions = [ 24 | perform_some_action, 25 | (perform_a_maintenance_action, ('media_tree.manage_filenode',)) 26 | ] 27 | """ 28 | 29 | @classmethod 30 | def contribute(extender, extended_class=FileNodeAdmin): 31 | super(AdminExtender, extender).contribute(extended_class) 32 | if extender.actions: 33 | for action in extender.actions: 34 | if isinstance(action, tuple): 35 | extended_class.register_action(*action) 36 | else: 37 | extended_class.register_action(action) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Samuel Luescher 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /demo_project/media/upload/license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Samuel Luescher 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /media_tree/migrations/0002_mptt_to_treebeard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('media_tree', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='filenode', 16 | options={'verbose_name': 'media object', 'verbose_name_plural': 'media objects', 'permissions': (('manage_filenode', 'Can perform management tasks'),)}, 17 | ), 18 | migrations.RenameField( 19 | model_name='filenode', 20 | old_name='level', 21 | new_name='depth' 22 | ), 23 | migrations.RenameField( 24 | model_name='filenode', 25 | old_name='rght', 26 | new_name='rgt' 27 | ), 28 | migrations.RemoveField( 29 | model_name='filenode', 30 | name='parent', 31 | ), 32 | 33 | migrations.AlterField( 34 | model_name='filenode', 35 | name='depth', 36 | field=models.PositiveIntegerField(db_index=True), 37 | ), 38 | migrations.AlterField( 39 | model_name='filenode', 40 | name='lft', 41 | field=models.PositiveIntegerField(db_index=True), 42 | ), 43 | migrations.AlterField( 44 | model_name='filenode', 45 | name='rgt', 46 | field=models.PositiveIntegerField(db_index=True), 47 | ), 48 | migrations.AlterField( 49 | model_name='filenode', 50 | name='tree_id', 51 | field=models.PositiveIntegerField(db_index=True), 52 | ) 53 | ] 54 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/focal_point/media_extension.py: -------------------------------------------------------------------------------- 1 | from media_tree import extension 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django.core.validators import MinValueValidator, MaxValueValidator 5 | 6 | 7 | class FocalPointModelExtender(extension.ModelExtender): 8 | 9 | focal_x = models.DecimalField(_('Focal point X'), blank=True, null=True, max_digits=5, 10 | decimal_places=3, validators=[MinValueValidator(0), MaxValueValidator(1)]) 11 | focal_y = models.DecimalField(_('Focal point Y'), blank=True, null=True, max_digits=5, 12 | decimal_places=3, validators=[MinValueValidator(0), MaxValueValidator(1)], 13 | help_text=_('Drag the marker on the image thumbnail to its most relevant portion.' \ 14 | ' You can then use this information to crop the image accordingly.')) 15 | 16 | def get_crop(self): 17 | x = '' 18 | y = '' 19 | if self.focal_x != None: 20 | x = str(int(round(self.focal_x * 100))) 21 | if self.focal_y != None: 22 | y = str(int(round(self.focal_y * 100))) 23 | return "%s,%s" % (x, y) 24 | 25 | 26 | class FocalPointFormExtender(extension.FormExtender): 27 | 28 | class Media: 29 | js = ( 30 | 'focal_point/js/focal_point.js', 31 | ) 32 | css = { 33 | 'all': ( 34 | 'focal_point/css/focal_point.css', 35 | ) 36 | } 37 | 38 | class Meta: 39 | fieldsets = [ 40 | (_('Focal point'), { 41 | 'fields': ['focal_x', 'focal_y'], 42 | 'classes': ['collapse'] 43 | }) 44 | ] 45 | 46 | 47 | extension.register(FocalPointModelExtender) 48 | extension.register(FocalPointFormExtender) 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/fields.rst: -------------------------------------------------------------------------------- 1 | .. _fields: 2 | 3 | Fields and forms 4 | **************** 5 | 6 | There are a number of field classes for conveniently using ``FileNode`` objects 7 | in your own Django applications. 8 | 9 | The following example model contains a ``ForeignKey`` field linking to a 10 | ``FileNode`` object that is associated to a document file. Notice the parameters 11 | specifying which media types will be validated, and which should be visible in 12 | the widget:: 13 | 14 | from media_tree.fields import FileNodeForeignKey 15 | from media_tree import media_types 16 | from media_tree.models import FileNode 17 | from django.db import models 18 | 19 | class MyModel(models.Model): 20 | document = FileNodeForeignKey(allowed_media_types=(media_types.DOCUMENT,), 21 | null=True, limit_choices_to={'media_type__in': 22 | (FileNode.FOLDER, media_types.DOCUMENT)}) 23 | 24 | The following example model will allow the user to select a ``FileNode`` object 25 | associated to an image file:: 26 | 27 | from media_tree.fields import ImageFileNodeForeignKey 28 | from django.db import models 29 | 30 | class MyModel(models.Model): 31 | image_node = ImageFileNodeForeignKey(null=True) 32 | 33 | The following example form will allow the user to select files that are under a 34 | specific parent folder named “Projects”:: 35 | 36 | from media_tree.models import FileNode 37 | from media_tree.forms import FileNodeChoiceField 38 | from django import forms 39 | 40 | class MyForm(forms.Form): 41 | file_node = FileNodeChoiceField(queryset=FileNode.objects.get( 42 | name="Projects", node_type=FileNode.FOLDER).get_descendants()) 43 | 44 | For your own applications, the following field classes are available: 45 | 46 | .. automodule:: media_tree.fields 47 | :members: 48 | -------------------------------------------------------------------------------- /media_tree/extension/base_extenders/__init__.py: -------------------------------------------------------------------------------- 1 | from django.forms.widgets import MediaDefiningClass 2 | from media_tree.admin.forms import FileForm 3 | 4 | 5 | class MediaTreeExtender(object): 6 | 7 | @classmethod 8 | def contribute(extender, extended_class=None): 9 | """The `contribute` method contributes the defined extender attributes 10 | to its `extended_class`. If you need to change the `extended_class` in 11 | ways other than what's described here, you can extend this method. 12 | """ 13 | raise NotImplementedError('Class `%s` has not implemented a `contribute()` method.' % extender) 14 | 15 | 16 | class MediaDefiningExtender(MediaTreeExtender): 17 | __metaclass__ = MediaDefiningClass 18 | 19 | @classmethod 20 | def contribute(extender, extended_class=FileForm): 21 | # TODO this should raise a NotImplementedError if class does not 22 | # define its own contribute() 23 | 24 | # TODO: Instantiating the FileNodeAdmin like this is invalid 25 | if getattr(extender, 'Media', None): 26 | combined_media = extended_class().media + extender().media 27 | class NewMediaClass: 28 | js = combined_media._js 29 | css = combined_media._css 30 | extended_class.Media = NewMediaClass 31 | # TODO: what about the `extend` property? Media extender should 32 | # be able to override media instead of extending. 33 | 34 | # TODO: Maybe do this more elegantly = SEE: 35 | # https://docs.djangoproject.com/en/dev/topics/forms/media/#media-as-a-dynamic-property 36 | # 37 | # def _media(self): 38 | # return forms.Media(css={'all': ('pretty.css',)}, 39 | # js=('animations.js', 'actions.js')) 40 | # media = property(_media) 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Media Tree Documentation 2 | ******************************* 3 | 4 | Introduction 5 | ============ 6 | 7 | Django Media Tree is a Django app for managing your website's media files in a 8 | folder tree, and using them in your own applications. 9 | 10 | .. image:: _static/admin-screenshot.png 11 | 12 | Key features: 13 | 14 | * Enables you to organize all of your site media in nested folders. 15 | * Supports various media types (images, audio, video, archives etc). 16 | * Extension system, enabling you to easily add special processing for different 17 | media types and extend the admin interface. 18 | * Speedy AJAX-enhanced admin interface with drag & drop and dynamic resizing. 19 | * Upload queue with progress indicators (using Fine Uploader). 20 | * Add metadata to all media to improve accessibility of your web sites. 21 | * Integration with `Django CMS `_. Plugins include: 22 | image, slideshow, gallery, download list -- create your own! 23 | 24 | 25 | The Media Tree application 26 | ========================== 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | 31 | installation 32 | admin 33 | models 34 | configuration 35 | utils 36 | bundled_extensions 37 | management 38 | 39 | 40 | Extending und using Media Tree with other applications 41 | ====================================================== 42 | 43 | Your choices range from implementing file listing and detail views based on the 44 | bundled generic view classes, extending Media Tree itself and its admin 45 | interface, or writing custom plugins for use with your own applications. 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | 50 | fields 51 | templates 52 | views 53 | extending 54 | custom_plugins 55 | bundled_plugins 56 | 57 | 58 | Indices and tables 59 | ================== 60 | 61 | * :ref:`genindex` 62 | * :ref:`modindex` 63 | * :ref:`search` 64 | 65 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/models.py: -------------------------------------------------------------------------------- 1 | from media_tree import media_types 2 | from media_tree.contrib.cms_plugins import settings as plugins_settings 3 | from media_tree.fields import FileNodeForeignKey, DimensionField 4 | from cms.models import CMSPlugin, Page 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | class MediaTreeImage(CMSPlugin): 10 | node = FileNodeForeignKey(allowed_media_types=(media_types.SUPPORTED_IMAGE,), verbose_name=_('file')) 11 | link_type = models.CharField(_('link type'), max_length=1, blank=True, null=True, default=plugins_settings.MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_DEFAULT, choices=plugins_settings.MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_CHOICES, help_text=_('Makes the image a clickable link.')) 12 | link_page = models.ForeignKey(Page, verbose_name=_("page"), null=True, blank=True, help_text=_('For link to page. Select any page from the list.')) 13 | link_url = models.CharField(_("web address"), max_length=255, blank=True, null=True, help_text=_('For link to web address. Example: Enter "http://www.domain.com" to create an absolute link to an external site, or enter a relative URL like "/about/contact".')) 14 | link_target = models.CharField(_("link target"), max_length=64, blank=True, null=True, choices=(('', _('same window')), ('_blank', _('new window')))) 15 | width = DimensionField(_('max. width'), null=True, blank=True, help_text=_('You can leave this empty to use an automatically determined image width.')) 16 | height = DimensionField(_('max. height'), null=True, blank=True, help_text=_('You can leave this empty to use an automatically determined image height.')) 17 | render_template = models.CharField(_("template"), max_length=100, choices=None, blank=True, null=True, help_text=_('Template used to render the image.')) 18 | 19 | def __unicode__(self): 20 | return self.node.__unicode__() 21 | -------------------------------------------------------------------------------- /media_tree/contrib/media_backends/easy_thumbnails/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree.media_backends import MediaBackend, ThumbnailError 2 | from media_tree.utils import get_media_storage 3 | from media_tree import settings as app_settings 4 | from media_tree import media_types 5 | from easy_thumbnails.files import get_thumbnailer 6 | from easy_thumbnails import utils 7 | from django.conf import settings 8 | import os 9 | 10 | 11 | class EasyThumbnailsBackend(MediaBackend): 12 | """ 13 | Media backend for easy_thumbnails support. 14 | """ 15 | 16 | SUPPORTED_MEDIA_TYPES = (media_types.SUPPORTED_IMAGE,) 17 | 18 | @staticmethod 19 | def supports_thumbnails(): 20 | return True 21 | 22 | @staticmethod 23 | def check_conf(): 24 | if not 'easy_thumbnails' in settings.INSTALLED_APPS: 25 | from django.core.exceptions import ImproperlyConfigured 26 | raise ImproperlyConfigured('`easy_thumbnails` is not in your INSTALLED_APPS.') 27 | 28 | @staticmethod 29 | def get_thumbnail(source, options): 30 | try: 31 | opts = {} 32 | opts.update(app_settings.MEDIA_TREE_GLOBAL_THUMBNAIL_OPTIONS or {}) 33 | opts.update(options) 34 | thumbnail = get_thumbnailer(source).get_thumbnail(opts) 35 | except Exception as inst: 36 | EasyThumbnailsBackend.check_conf() 37 | if app_settings.MEDIA_TREE_MEDIA_BACKEND_DEBUG: 38 | raise ThumbnailError(inst) 39 | else: 40 | return None 41 | return thumbnail 42 | 43 | @staticmethod 44 | def get_valid_thumbnail_options(): 45 | options = utils.valid_processor_options() 46 | options.remove('size') 47 | return options 48 | 49 | @staticmethod 50 | def get_cache_paths(subdirs=None): 51 | if hasattr(settings, 'THUMBNAIL_SUBDIR'): 52 | return MediaBackend.get_cache_paths((settings.THUMBNAIL_SUBDIR,)) 53 | return () -------------------------------------------------------------------------------- /media_tree/contrib/views/mixin_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | You can use Mixins as superclasses for your custom plugins when 3 | interfacing with third-party applications, such as Django CMS. Please 4 | take a look at :ref:`custom-plugins-howto` for more information. 5 | 6 | Basically, a Mixin classes adds methods to your own class (which is 7 | subclassing a Mixin) for instantiating View classes. All attributes 8 | of your own class that also exist in the View class will be used to 9 | initialize View instances. 10 | 11 | For instance, if your custom class has an attribute 12 | ``template_name``, and an attribute with the same name also 13 | exists in the View class, then the View instance's 14 | ``template_name`` attribute will be set accordingly. 15 | 16 | Please refer to :ref:`generic-views` for an overview of attributes you can 17 | define. 18 | """ 19 | 20 | VALID_MIXIN_OPTIONS = {} 21 | 22 | class PluginMixin(object): 23 | 24 | def get_view(self, request, view_class, opts=None): 25 | """ 26 | Instantiates and returns the view class that will generate the 27 | actual context for this plugin. 28 | """ 29 | kwargs = {} 30 | if opts: 31 | if not isinstance(opts, dict): 32 | opts = opts.__dict__ 33 | else: 34 | opts = {} 35 | 36 | if not view_class in VALID_MIXIN_OPTIONS: 37 | valid_options = view_class.__dict__.keys() 38 | for cls in view_class.__bases__: 39 | if cls != object: 40 | valid_options += cls.__dict__.keys() 41 | VALID_MIXIN_OPTIONS[view_class] = valid_options 42 | 43 | for key in VALID_MIXIN_OPTIONS[view_class]: 44 | if key in opts: 45 | kwargs[key] = opts[key] 46 | elif hasattr(self, key): 47 | kwargs[key] = getattr(self, key) 48 | 49 | view = view_class(**kwargs) 50 | view.request = request 51 | view.kwargs = {} 52 | return view 53 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/views.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_image.models import MediaTreeImage 2 | from media_tree.contrib.cms_plugins.helpers import PluginLink 3 | from media_tree.models import FileNode 4 | from media_tree.contrib.views.detail.image import ImageNodeDetailView 5 | from django.utils.translation import ugettext_lazy as _ 6 | from cms.utils.page_resolver import get_page_from_path 7 | from django.http import Http404 8 | 9 | 10 | class ImagePluginDetailView(ImageNodeDetailView): 11 | 12 | return_url = None 13 | 14 | def get_object(self, *args, **kwargs): 15 | obj = super(ImagePluginDetailView, self).get_object(*args, **kwargs) 16 | if obj: 17 | allowed = False 18 | # validate that the object is actually published using the plugin... 19 | for plugin in MediaTreeImage.objects.filter(node=obj): 20 | # ...and on a publicly accessible page. 21 | # TODO: Iterating all plugins and getting each page 22 | # is a bit inefficient. 23 | page = get_page_from_path(plugin.page.get_path()) 24 | if page: 25 | allowed = True 26 | break 27 | if not allowed: 28 | raise Http404 29 | return obj 30 | 31 | def get_context_data(self, *args, **kwargs): 32 | context_data = super(ImagePluginDetailView, self).get_context_data( 33 | *args, **kwargs) 34 | 35 | if self.return_url: 36 | page = get_page_from_path(self.return_url.strip('/')) 37 | if page: 38 | context_data.update({ 39 | 'link': PluginLink(url=page.get_absolute_url(), 40 | text=_('Back to %s') % page.get_title()) 41 | }) 42 | 43 | return context_data 44 | 45 | def get(self, request, *args, **kwargs): 46 | self.return_url = request.GET.get('return_url', None) 47 | return super(ImagePluginDetailView, self).get(request, *args, **kwargs) -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_listing/models.py: -------------------------------------------------------------------------------- 1 | from media_tree import media_types 2 | from media_tree.contrib.cms_plugins import settings as app_settings 3 | from media_tree.models import FileNode 4 | from media_tree.fields import FileNodeForeignKey 5 | from cms.models import CMSPlugin 6 | from django.db import models 7 | from django.utils.translation import ugettext_lazy as _ 8 | from media_tree.contrib.views.listing import LISTING_MERGED, LISTING_NESTED 9 | 10 | 11 | class MediaTreeListingBase(CMSPlugin): 12 | 13 | render_template = models.CharField(_('template'), max_length=100, choices=None, blank=True, null=True, help_text=_('Template used to render the plugin.')) 14 | filename_filter = models.CharField(_('filter file and folder names'), max_length=255, null=True, blank=True, help_text=_('Example: *.jpg; Documents.*;'), editable=False) 15 | include_descendants = models.BooleanField(_('include all subfolders'), default=True) 16 | 17 | class Meta: 18 | abstract = True 19 | 20 | def __unicode__(self): 21 | # TODO return node list 22 | return '' 23 | 24 | 25 | class MediaTreeListingItemBase(models.Model): 26 | position = models.IntegerField(_('position'), blank=True) 27 | 28 | class Meta: 29 | abstract = True 30 | ordering = ['position', 'id'] 31 | verbose_name = _('media object') 32 | verbose_name_plural = _('media objects') 33 | 34 | def save(self, *args, **kwargs): 35 | if self.position == None: 36 | self.position = 0 37 | super(MediaTreeListingItemBase, self).save(*args, **kwargs) 38 | 39 | def __unicode__(self): 40 | return self.node.name 41 | 42 | 43 | class MediaTreeListing(MediaTreeListingBase): 44 | list_type = models.CharField(_('List type'), max_length=1, default=LISTING_NESTED, choices=((LISTING_MERGED, _('merged')), (LISTING_NESTED, _('nested')))) 45 | 46 | 47 | class MediaTreeListingItem(MediaTreeListingItemBase): 48 | list_plugin = models.ForeignKey(MediaTreeListing, related_name='media_items') 49 | node = FileNodeForeignKey(verbose_name=_('folder/file')) 50 | -------------------------------------------------------------------------------- /media_tree/locale/en/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-09-28 13:43-0400\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: static/media_tree/js/admin_enhancements.js:6 20 | msgid "Name" 21 | msgstr "" 22 | 23 | #: static/media_tree/js/admin_enhancements.js:6 24 | msgid "Size" 25 | msgstr "" 26 | 27 | #: static/media_tree/js/admin_enhancements.js:39 28 | msgid "New folder" 29 | msgstr "" 30 | 31 | #: static/media_tree/js/admin_enhancements.js:40 32 | msgid "Save" 33 | msgstr "" 34 | 35 | #: static/media_tree/js/admin_enhancements.js:198 36 | #, perl-format 37 | msgid "%i media object" 38 | msgid_plural "%i media objects" 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: static/media_tree/js/jquery.swfupload_manager.js:84 43 | #, perl-format 44 | msgid "uploading… (%i in queue)" 45 | msgstr "" 46 | 47 | #: static/media_tree/js/jquery.swfupload_manager.js:85 48 | #, perl-format 49 | msgid "%i file in queue." 50 | msgid_plural "%i files in queue." 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | #: static/media_tree/js/jquery.swfupload_manager.js:86 55 | msgid "Please do not close this window until upload is complete." 56 | msgstr "" 57 | 58 | #: static/media_tree/js/jquery.swfupload_manager.js:118 59 | msgid "Upload error" 60 | msgstr "" 61 | 62 | #: static/media_tree/js/jquery.swfupload_manager.js:225 63 | msgid "loading…" 64 | msgstr "" 65 | 66 | #: static/media_tree/js/jquery.swfupload_manager.js:235 67 | #, perl-format 68 | msgid "Successfully added %i file." 69 | msgid_plural "Successfully added %i files." 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | #: static/media_tree/js/jquery.swfupload_manager.js:243 74 | msgid "There were errors during upload." 75 | msgstr "" 76 | -------------------------------------------------------------------------------- /media_tree/utils/maintenance.py: -------------------------------------------------------------------------------- 1 | from media_tree.utils import get_media_storage 2 | from media_tree.media_backends import get_media_backend 3 | from media_tree.models import FileNode 4 | from media_tree import settings as app_settings 5 | from unicodedata import normalize 6 | import os 7 | 8 | 9 | def get_cache_files(): 10 | storage = get_media_storage() 11 | cache_files = [] 12 | 13 | for cache_dir in get_media_backend().get_cache_paths(): 14 | if storage.exists(cache_dir): 15 | files_in_dir = [storage.path(os.path.join(cache_dir, filename)) \ 16 | for filename in storage.listdir(cache_dir)[1]] 17 | for file_path in files_in_dir: 18 | # need to normalize unicode path due to https://code.djangoproject.com/ticket/16315 19 | file_path = normalize('NFC', file_path) 20 | storage_name = os.path.join(cache_dir, os.path.basename(file_path)) 21 | cache_files.append(storage_name) 22 | 23 | return cache_files 24 | 25 | 26 | def get_broken_media(): 27 | storage = get_media_storage() 28 | media_subdir = app_settings.MEDIA_TREE_UPLOAD_SUBDIR 29 | broken_nodes = [] 30 | orphaned_files = [] 31 | 32 | files_in_db = [] 33 | for node in FileNode.objects.filter(node_type=FileNode.FILE): 34 | path = node.file.path 35 | # need to normalize unicode path due to https://code.djangoproject.com/ticket/16315 36 | path = normalize('NFC', path) 37 | files_in_db.append(path) 38 | if not storage.exists(node.file): 39 | broken_nodes.append(node) 40 | 41 | files_in_storage = [storage.path(os.path.join(media_subdir, filename)) \ 42 | for filename in storage.listdir(media_subdir)[1]] 43 | 44 | for file_path in files_in_storage: 45 | # need to normalize unicode path due to https://code.djangoproject.com/ticket/16315 46 | file_path = normalize('NFC', file_path) 47 | if not file_path in files_in_db: 48 | storage_name = os.path.join(media_subdir, os.path.basename(file_path)) 49 | orphaned_files.append(storage_name) 50 | 51 | return [broken_nodes, orphaned_files] 52 | 53 | 54 | def get_orphaned_files(): 55 | return get_broken_media()[1] 56 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_gallery/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_gallery.models import MediaTreeGallery, MediaTreeGalleryItem 2 | from media_tree.contrib.cms_plugins.media_tree_slideshow.cms_plugins import MediaTreeSlideshowPlugin 3 | from media_tree.contrib.views.listing import FileNodeListingMixin 4 | from media_tree.contrib.cms_plugins.forms import MediaTreePluginFormInlinePositioningBase 5 | from media_tree.models import FileNode 6 | from media_tree import media_types 7 | from media_tree.utils.filenode import get_nested_filenode_list 8 | from cms.plugin_pool import plugin_pool 9 | from django.utils.translation import ugettext_lazy as _ 10 | from django.contrib import admin 11 | from django.http import Http404 12 | 13 | 14 | class MediaTreeGalleryPluginForm(MediaTreePluginFormInlinePositioningBase): 15 | class Meta: 16 | model = MediaTreeGallery 17 | fields = '__all__' 18 | 19 | 20 | class MediaTreeGalleryItemInline(admin.StackedInline): 21 | model = MediaTreeGalleryItem 22 | extra = 0 23 | 24 | 25 | # TODO The default output/template does not make much sense. Include: Previews, folder thumbnails, collapsible tree etc. 26 | class MediaTreeGalleryPlugin(MediaTreeSlideshowPlugin, FileNodeListingMixin): 27 | model = MediaTreeGallery 28 | module = _('Media Tree') 29 | name = _('Gallery') 30 | admin_preview = False 31 | form = MediaTreeGalleryPluginForm 32 | fieldsets = [ 33 | (_('Settings'), { 34 | 'fields': form().fields.keys(), 35 | 'classes': ['collapse'] 36 | }), 37 | ] 38 | inlines = [MediaTreeGalleryItemInline] 39 | render_template = 'cms/plugins/media_tree_gallery.html' 40 | 41 | class PluginMedia: 42 | js = [ 43 | 'lib/jquery.cycle/jquery.cycle.all.min.js', 44 | ] 45 | 46 | filter_by_parent_folder = True 47 | list_filter_media_types = (media_types.SUPPORTED_IMAGE,) 48 | 49 | def render(self, context, instance, placeholder): 50 | context = super(MediaTreeGalleryPlugin, self).render(context, instance, placeholder) 51 | context.update({ 52 | 'auto_play': instance.auto_play, 53 | }); 54 | 55 | return context 56 | 57 | 58 | plugin_pool.register_plugin(MediaTreeGalleryPlugin) 59 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_listing/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_listing.models import MediaTreeListing, MediaTreeListingItem 2 | from media_tree.contrib.cms_plugins.forms import MediaTreePluginFormInlinePositioningBase 3 | from media_tree.contrib.views.listing import FileNodeListingFilteredByFolderMixin 4 | from cms.plugin_base import CMSPluginBase 5 | from cms.plugin_pool import plugin_pool 6 | from django.utils.translation import ugettext_lazy as _ 7 | from django.contrib import admin 8 | 9 | 10 | class MediaTreeListingPluginForm(MediaTreePluginFormInlinePositioningBase): 11 | class Meta: 12 | model = MediaTreeListing 13 | fields = '__all__' 14 | 15 | 16 | class MediaTreeListingItemInline(admin.StackedInline): 17 | model = MediaTreeListingItem 18 | extra = 0 19 | 20 | 21 | class MediaTreeListingPlugin(CMSPluginBase, FileNodeListingFilteredByFolderMixin): 22 | model = MediaTreeListing 23 | module = _('Media Tree') 24 | name = _('File listing') 25 | admin_preview = False 26 | render_template = 'cms/plugins/media_tree_listing.html' 27 | form = MediaTreeListingPluginForm 28 | inlines = [MediaTreeListingItemInline] 29 | fieldsets = [ 30 | (_('Settings'), { 31 | 'fields': form().fields.keys(), 32 | 'classes': ['collapse'] 33 | }), 34 | ] 35 | 36 | filter_by_parent_folder = False 37 | 38 | def render(self, context, instance, placeholder): 39 | selected_nodes = [item.node for item in instance.media_items.all()] 40 | if hasattr(instance, 'filter_supported') and not getattr(instance, 'filter_supported'): 41 | self.list_filter_media_types = None 42 | view = self.get_listing_view(context['request'], selected_nodes, opts=instance) 43 | if hasattr(instance, 'include_descendants'): 44 | # For each selected folder, its children should be visible. 45 | # Hence override include_descendants and solve with max_depth 46 | view.include_descendants = True 47 | if not getattr(instance, 'include_descendants'): 48 | view.list_max_depth = 2 49 | view.folder_pk_param_name = 'folder-%i' % instance.pk 50 | context.update(view.get_context_data()) 51 | return context 52 | 53 | 54 | plugin_pool.register_plugin(MediaTreeListingPlugin) 55 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/helpers.py: -------------------------------------------------------------------------------- 1 | from media_tree.utils.filenode import get_file_link 2 | from django.core.urlresolvers import reverse 3 | from django.utils.safestring import mark_safe 4 | from django.core.urlresolvers import NoReverseMatch 5 | 6 | 7 | class PluginLink(object): 8 | 9 | LINK_PAGE = 'P' 10 | LINK_URL = 'U' 11 | LINK_IMAGE_DETAIL = 'I' 12 | LINK_URL_REVERSE = 'R' 13 | 14 | def __init__(self, type=LINK_URL, url=None, text='', obj=None, rel=None, target=None, querystring=''): 15 | self.type = type 16 | self.url = url 17 | self.obj = obj 18 | self.target = target 19 | self.querystring = querystring 20 | self.text = text 21 | if self.type == PluginLink.LINK_IMAGE_DETAIL: 22 | self.url = ['media_tree.contrib.cms_plugins.media_tree_image.ImagePluginDetailView', self.obj.pk] 23 | self.rel = 'image-detail' 24 | 25 | def href(self): 26 | href = None 27 | if self.type == PluginLink.LINK_URL: 28 | href = self.url 29 | if self.type in (PluginLink.LINK_URL_REVERSE, PluginLink.LINK_IMAGE_DETAIL): 30 | if isinstance(self.url, basestring): 31 | parts = self.url.split(' ') 32 | else: 33 | parts = self.url 34 | name = parts.pop(0) 35 | try: 36 | href = reverse(name, args=parts) 37 | except NoReverseMatch: 38 | raise 39 | if self.type == PluginLink.LINK_PAGE: 40 | href = self.obj.get_absolute_url() 41 | if href != None: 42 | href += self.querystring 43 | return href 44 | 45 | @staticmethod 46 | def create_from(instance): 47 | if not getattr(instance, 'link_type', None): 48 | return None 49 | querystring = '' 50 | if instance.link_type == PluginLink.LINK_PAGE: 51 | link_obj = instance.link_page 52 | elif instance.link_type == PluginLink.LINK_IMAGE_DETAIL: 53 | link_obj = instance.node 54 | if getattr(instance, 'page', None): 55 | querystring = '?return_url=%s' % instance.page.get_absolute_url() 56 | else: 57 | link_obj = None 58 | return PluginLink(instance.link_type, url=instance.link_url, obj=link_obj, target=instance.link_target, querystring=querystring) 59 | 60 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_image/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_image.models import MediaTreeImage 2 | from media_tree.contrib.cms_plugins.forms import MediaTreePluginFormBase 3 | from media_tree.contrib.views.detail.image import ImageNodeDetailMixin 4 | from media_tree import media_types 5 | from media_tree.media_backends import get_media_backend 6 | from media_tree.contrib.cms_plugins.helpers import PluginLink 7 | from cms.plugin_base import CMSPluginBase 8 | from cms.plugin_pool import plugin_pool 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | # TODO: Solve image_detail with get_absolute_url()? 12 | 13 | 14 | class MediaTreeImagePluginForm(MediaTreePluginFormBase): 15 | class Meta: 16 | model = MediaTreeImage 17 | fields = '__all__' 18 | 19 | 20 | class MediaTreeImagePlugin(CMSPluginBase, ImageNodeDetailMixin): 21 | model = MediaTreeImage 22 | module = _('Media Tree') 23 | name = _("Image") 24 | admin_preview = False 25 | render_template = 'cms/plugins/media_tree_image.html' 26 | text_enabled = True 27 | form = MediaTreeImagePluginForm 28 | 29 | fieldsets = [ 30 | (_('Image'), { 31 | 'fields': ['node'], 32 | }), 33 | (_('Settings'), { 34 | 'fields': ['width', 'height'], 35 | 'classes': ['collapse'], 36 | }), 37 | (_('Link'), { 38 | 'fields': ['link_type', 'link_url', 'link_page', 'link_target'], 39 | 'classes': ['collapse'], 40 | }), 41 | ] 42 | exclude = ('body', 'render_template') 43 | 44 | def render(self, context, instance, placeholder): 45 | view = self.get_detail_view(context['request'], instance.node, opts=instance) 46 | context.update(view.get_context_data()) 47 | if instance.link_type: 48 | context[view.context_object_name].link = PluginLink.create_from(instance) 49 | 50 | return context 51 | 52 | def icon_src(self, instance): 53 | media_backend = get_media_backend(fail_silently=False, handles_media_types=( 54 | media_types.SUPPORTED_IMAGE,)) 55 | thumb = media_backend.get_thumbnail(instance.node.file, {'size': (200, 200)}) 56 | return thumb.url 57 | 58 | def icon_alt(self, instance): 59 | return instance.node.alt 60 | 61 | 62 | plugin_pool.register_plugin(MediaTreeImagePlugin) 63 | -------------------------------------------------------------------------------- /media_tree/locale/de/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PACKAGE VERSION\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-09-28 13:43-0400\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: static/media_tree/js/admin_enhancements.js:6 20 | msgid "Name" 21 | msgstr "Name" 22 | 23 | #: static/media_tree/js/admin_enhancements.js:6 24 | msgid "Size" 25 | msgstr "Grösse" 26 | 27 | #: static/media_tree/js/admin_enhancements.js:39 28 | msgid "New folder" 29 | msgstr "Neuer Ordner" 30 | 31 | #: static/media_tree/js/admin_enhancements.js:40 32 | msgid "Save" 33 | msgstr "Speichern" 34 | 35 | #: static/media_tree/js/admin_enhancements.js:198 36 | #, perl-format 37 | msgid "%i media object" 38 | msgid_plural "%i media objects" 39 | msgstr[0] "%i Medienobjekt" 40 | msgstr[1] "%i Medienobjekte" 41 | 42 | #: static/media_tree/js/jquery.swfupload_manager.js:84 43 | #, perl-format 44 | msgid "uploading… (%i in queue)" 45 | msgstr "Upload… (%i in Warteschlange)" 46 | 47 | #: static/media_tree/js/jquery.swfupload_manager.js:85 48 | #, perl-format 49 | msgid "%i file in queue." 50 | msgid_plural "%i files in queue." 51 | msgstr[0] "%i Datei in Warteschlange." 52 | msgstr[1] "%i Dateien in Warteschlange." 53 | 54 | #: static/media_tree/js/jquery.swfupload_manager.js:86 55 | msgid "Please do not close this window until upload is complete." 56 | msgstr "" 57 | "Bitte dieses Fenster nicht schliessen, bevor der Upload abgeschlossen ist." 58 | 59 | #: static/media_tree/js/jquery.swfupload_manager.js:118 60 | msgid "Upload error" 61 | msgstr "Upload-Fehler" 62 | 63 | #: static/media_tree/js/jquery.swfupload_manager.js:225 64 | msgid "loading…" 65 | msgstr "laden…" 66 | 67 | #: static/media_tree/js/jquery.swfupload_manager.js:235 68 | #, perl-format 69 | msgid "Successfully added %i file." 70 | msgid_plural "Successfully added %i files." 71 | msgstr[0] "Erfolgreich %i Datei hinzugefügt." 72 | msgstr[1] "Erfolgreich %i Dateien hinzugefügt." 73 | 74 | #: static/media_tree/js/jquery.swfupload_manager.js:243 75 | msgid "There were errors during upload." 76 | msgstr "Beim Upload sind Fehler aufgetreten." 77 | -------------------------------------------------------------------------------- /media_tree/locale/nb/LC_MESSAGES/djangojs.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-media-tree\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-09-28 13:43-0400\n" 11 | "PO-Revision-Date: 2012-12-29 22:52+0100\n" 12 | "Last-Translator: Erlend Dalen \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | "X-Generator: Poedit 1.5.4\n" 19 | "X-Poedit-SourceCharset: UTF-8\n" 20 | 21 | #: static/media_tree/js/admin_enhancements.js:6 22 | msgid "Name" 23 | msgstr "Navn" 24 | 25 | #: static/media_tree/js/admin_enhancements.js:6 26 | msgid "Size" 27 | msgstr "Størrekse" 28 | 29 | #: static/media_tree/js/admin_enhancements.js:39 30 | msgid "New folder" 31 | msgstr "Ny mappe" 32 | 33 | #: static/media_tree/js/admin_enhancements.js:40 34 | msgid "Save" 35 | msgstr "Lagre" 36 | 37 | #: static/media_tree/js/admin_enhancements.js:198 38 | #, perl-format 39 | msgid "%i media object" 40 | msgid_plural "%i media objects" 41 | msgstr[0] "%i media objekt" 42 | msgstr[1] "%i media objekter" 43 | 44 | #: static/media_tree/js/jquery.swfupload_manager.js:84 45 | #, perl-format 46 | msgid "uploading… (%i in queue)" 47 | msgstr "laster opp… (%i i køen)" 48 | 49 | #: static/media_tree/js/jquery.swfupload_manager.js:85 50 | #, perl-format 51 | msgid "%i file in queue." 52 | msgid_plural "%i files in queue." 53 | msgstr[0] "%i file i køen" 54 | msgstr[1] "%i filer i køen" 55 | 56 | #: static/media_tree/js/jquery.swfupload_manager.js:86 57 | msgid "Please do not close this window until upload is complete." 58 | msgstr "Vennligst ikke lukk dette vinduet før opplastingen er ferdig" 59 | 60 | #: static/media_tree/js/jquery.swfupload_manager.js:118 61 | msgid "Upload error" 62 | msgstr "Feil ved opplasting" 63 | 64 | #: static/media_tree/js/jquery.swfupload_manager.js:225 65 | msgid "loading…" 66 | msgstr "laster…" 67 | 68 | #: static/media_tree/js/jquery.swfupload_manager.js:235 69 | #, perl-format 70 | msgid "Successfully added %i file." 71 | msgid_plural "Successfully added %i files." 72 | msgstr[0] "%i fil ble lastet opp." 73 | msgstr[1] "%i filer ble lastet opp." 74 | 75 | #: static/media_tree/js/jquery.swfupload_manager.js:243 76 | msgid "There were errors during upload." 77 | msgstr "En feil oppstod ved opplasting." 78 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/forms.py: -------------------------------------------------------------------------------- 1 | from media_tree.models import FileNode 2 | from media_tree.contrib.cms_plugins.helpers import PluginLink 3 | from django import forms 4 | from django.contrib import admin 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.core.urlresolvers import NoReverseMatch 7 | 8 | 9 | class MediaTreePluginFormBase(forms.ModelForm): 10 | 11 | def clean_link_type(self): 12 | if 'node' in self.cleaned_data: 13 | # TODO: Links for folders, for instance link to full-size image for all slideshow items. 14 | # Could be achieved using FileNode.get_list() processors 15 | if self.cleaned_data['link_type'] and self.cleaned_data['node'].node_type == FileNode.FOLDER: 16 | raise forms.ValidationError(_('You can only link individual files, not folders.')) 17 | if self.cleaned_data['link_type'] == PluginLink.LINK_IMAGE_DETAIL: 18 | try: 19 | PluginLink(self.cleaned_data['link_type'], obj=self.cleaned_data['node']).href() 20 | except NoReverseMatch: 21 | raise forms.ValidationError(_('You need to attach the Media Tree Image Detail application to a page in order to link to full size images.')) 22 | return self.cleaned_data['link_type'] 23 | 24 | def clean_link_url(self): 25 | if self.cleaned_data.has_key('link_type') and self.cleaned_data['link_type'] in (PluginLink.LINK_URL, PluginLink.LINK_URL_REVERSE): 26 | if not self.cleaned_data['link_url']: 27 | raise forms.ValidationError(self.fields['link_url'].error_messages['required']) 28 | else: 29 | if not PluginLink(self.cleaned_data['link_type'], url=self.cleaned_data['link_url']).href(): 30 | raise forms.ValidationError(self.fields['link_url'].error_messages['invalid']) 31 | return self.cleaned_data['link_url'] 32 | 33 | def clean_link_page(self): 34 | if self.cleaned_data.has_key('link_type') and self.cleaned_data['link_type'] == PluginLink.LINK_PAGE and not self.cleaned_data['link_page']: 35 | raise forms.ValidationError(self.fields['link_page'].error_messages['required']) 36 | return self.cleaned_data['link_page'] 37 | 38 | 39 | class MediaTreePluginFormInlinePositioningBase(MediaTreePluginFormBase): 40 | 41 | class Media: 42 | js = [ 43 | 'cms/js/plugins/admincompat.js', 44 | 'cms/js/libs/jquery.ui.core.js', 45 | 'cms/js/libs/jquery.ui.sortable.js', 46 | 'media_tree/js/jquery-inline-positioning.js' 47 | ] 48 | -------------------------------------------------------------------------------- /media_tree/contrib/views/detail/image.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.views.detail import FileNodeDetailView, FileNodeDetailMixin 2 | from media_tree.utils import widthratio 3 | from media_tree import media_types 4 | 5 | 6 | class ImageNodeDetailView(FileNodeDetailView): 7 | """ 8 | View class for implementing image detail views for ``FileNode`` objects. 9 | This class is based on Django's generic ``DetailView``. Please refer to the 10 | respective Django documentation on subclassing and customizing `Class-based 11 | generic views 12 | `_. 13 | 14 | The following example ``urls.py`` would implement a detail view capable of 15 | displaying all image files under a folder node with the path 16 | ``some/folder``:: 17 | 18 | from media_tree.models import FileNode 19 | from media_tree.contrib.views.detail.image import ImageNodeDetailView 20 | from django.conf.urls.defaults import * 21 | 22 | urlpatterns = patterns('', 23 | (r'^images/(?P\d+)/$', ImageNodeDetailView.as_view( 24 | queryset=FileNode.objects.get(path='some/folder').get_descendants() 25 | )), 26 | ) 27 | """ 28 | 29 | width = None 30 | """ Maximum width of the thumbnail. If not set, default values will be used. """ 31 | 32 | height = None 33 | """ Maximum height of the thumbnail. If not set, default values will be used. """ 34 | 35 | context_object_name = 'image_node' 36 | """ Designates the name of the variable to use in the context. """ 37 | 38 | template_name = "media_tree/image_detail.html" 39 | """ Name of the template. """ 40 | 41 | filter_media_types = (media_types.SUPPORTED_IMAGE, media_types.VECTOR_IMAGE) 42 | 43 | 44 | def get_context_data(self, **kwargs): 45 | context = super(ImageNodeDetailView, self).get_context_data(**kwargs) 46 | 47 | if self.width or self.height: 48 | w = self.width or widthratio(self.height, self.object.height, self.object.width) 49 | h = self.height or widthratio(self.width, self.object.width, self.object.height) 50 | context.update({'thumbnail_size': (w, h)}) 51 | 52 | return context 53 | 54 | 55 | class ImageNodeDetailMixin(FileNodeDetailMixin): 56 | """ 57 | A mixin that you can use as a superclass for your own custom plugins 58 | for interfacing with third-party applications, such as Django CMS. Please 59 | take a look at :ref:`custom-plugins-howto` for more information. 60 | """ 61 | 62 | view_class = ImageNodeDetailView 63 | """ The view class instantiated by ``get_detail_view()``. """ 64 | 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | import os 3 | import sys 4 | from setuptools import setup, find_packages 5 | from setuptools.command.test import test as TestCommand 6 | try: # for pip >= 10 7 | from pip._internal.req import parse_requirements 8 | except ImportError: # for pip <= 9.0.3 9 | from pip.req import parse_requirements 10 | 11 | # parse_requirements() returns generator of pip.req.InstallRequirement objects 12 | install_reqs = parse_requirements('requirements.txt', session=False) 13 | 14 | # reqs is a list of requirement 15 | # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] 16 | REQS = [str(ir.requirement) for ir in install_reqs] 17 | 18 | def read(fname): 19 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 20 | README = read('README.rst') 21 | 22 | class Tox(TestCommand): 23 | user_options = [('tox-args=', 'a', "Arguments to pass to tox")] 24 | 25 | def initialize_options(self): 26 | TestCommand.initialize_options(self) 27 | self.tox_args = None 28 | 29 | def finalize_options(self): 30 | TestCommand.finalize_options(self) 31 | self.test_args = [] 32 | self.test_suite = True 33 | 34 | def run_tests(self): 35 | import tox 36 | import shlex 37 | args = self.tox_args 38 | if args: 39 | args = shlex.split(self.tox_args) 40 | errno = tox.cmdline(args=args) 41 | sys.exit(errno) 42 | 43 | setup( 44 | name = "django-media-tree", 45 | version = "0.9.0", 46 | install_requires=REQS, 47 | url = 'http://github.com/samluescher/django-media-tree', 48 | license = 'BSD', 49 | description="Django Media Tree is a Django app for managing your website's " 50 | "media files in a folder tree, and using them in your own applications.", 51 | long_description = README, 52 | 53 | author = u'Samuel Luescher', 54 | author_email = 'sam at samluescher dot net', 55 | 56 | packages = find_packages(), 57 | include_package_data=True, 58 | 59 | classifiers=[ 60 | 'Development Status :: 4 - Beta', 61 | 'Framework :: Django', 62 | 'Intended Audience :: Developers', 63 | 'License :: OSI Approved :: BSD License', 64 | 'Operating System :: OS Independent', 65 | 'Programming Language :: Python', 66 | 'Programming Language :: Python :: 2.6', 67 | 'Programming Language :: Python :: 2.7', 68 | 'Programming Language :: Python :: 3.2', 69 | 'Programming Language :: Python :: 3.3', 70 | 'Programming Language :: Python :: 3.4', 71 | 'Topic :: Internet :: WWW/HTTP', 72 | ], 73 | 74 | tests_require=['tox'], 75 | cmdclass={'test': Tox}, 76 | ) 77 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_slideshow/cms_plugins.py: -------------------------------------------------------------------------------- 1 | from media_tree.contrib.cms_plugins.media_tree_slideshow.models import MediaTreeSlideshow, MediaTreeSlideshowItem 2 | from media_tree.contrib.cms_plugins.media_tree_listing.cms_plugins import MediaTreeListingPlugin 3 | from media_tree.contrib.cms_plugins.media_tree_listing.models import MediaTreeListing 4 | from media_tree.contrib.cms_plugins.forms import MediaTreePluginFormInlinePositioningBase 5 | from media_tree.contrib.views.listing import LISTING_MERGED 6 | from media_tree import media_types 7 | from cms.plugin_base import CMSPluginBase 8 | from cms.plugin_pool import plugin_pool 9 | from django.utils.translation import ugettext_lazy as _ 10 | from django.contrib import admin 11 | 12 | 13 | class MediaTreeSlideshowPluginForm(MediaTreePluginFormInlinePositioningBase): 14 | class Meta: 15 | model = MediaTreeSlideshow 16 | fields = '__all__' 17 | 18 | 19 | class MediaTreeSlideshowItemInline(admin.StackedInline): 20 | model = MediaTreeSlideshowItem 21 | extra = 0 22 | fieldsets = [ 23 | ('', { 24 | 'fields': ['position', 'node'] 25 | }), 26 | (_('Link'), { 27 | 'fields': ['link_type', 'link_url', 'link_page', 'link_target'], 28 | 'classes': ['collapse'], 29 | }), 30 | ] 31 | 32 | 33 | class MediaTreeSlideshowPlugin(MediaTreeListingPlugin): 34 | model = MediaTreeSlideshow 35 | module = _('Media Tree') 36 | name = _('Slideshow') 37 | admin_preview = False 38 | render_template = 'cms/plugins/media_tree_slideshow.html' 39 | form = MediaTreeSlideshowPluginForm 40 | inlines = [MediaTreeSlideshowItemInline] 41 | fieldsets = [ 42 | (_('Settings'), { 43 | 'fields': form().fields.keys(), 44 | 'classes': ['collapse'] 45 | }), 46 | ] 47 | 48 | list_type = 'M' 49 | list_filter_media_types = (media_types.SUPPORTED_IMAGE,) 50 | 51 | class PluginMedia: 52 | js = [ 53 | 'lib/jquery.cycle/jquery.cycle.all.min.js', 54 | ] 55 | 56 | def render(self, context, instance, placeholder): 57 | context = super(MediaTreeSlideshowPlugin, self).render(context, instance, placeholder) 58 | context.update({ 59 | 'timeout': instance.timeout, 60 | 'fx': instance.fx, 61 | 'speed': instance.speed, 62 | }) 63 | if instance.width or instance.height: 64 | w = instance.width or instance.height 65 | h = instance.height or instance.width 66 | context.update({'thumbnail_size': (w, h)}) 67 | 68 | return context 69 | 70 | 71 | plugin_pool.register_plugin(MediaTreeSlideshowPlugin) 72 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/tree_change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/tree_change_list.html" %} 2 | {% load i18n media_tree_admin admin_list %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {% include "admin/media_tree/filenode/includes/uploader_button_js.html" %} 7 | {% comment %} 8 | Since the FileNodeAdmin's Media class can't use reverse to determine the jsi18n URL, 9 | it is embedded here. 10 | {% endcomment %} 11 | 12 | 13 | {% endblock %} 14 | 15 | {% block breadcrumbs %} 16 | {% if not node %} 17 | {{ block.super }} 18 | {% else %} 19 | {% include "admin/media_tree/filenode/includes/breadcrumbs.html" %} 20 | {% endif %} 21 | {% endblock %} 22 | 23 | {% block object-tools %} 24 | 46 | {% endblock %} 47 | 48 | {% block filters %} 49 |
50 |

{% trans 'View' %}

51 |
    52 | {% include "admin/media_tree/filenode/includes/list_type_controls.html" %} 53 |
54 | {% if thumbnail_sizes %} 55 |
    56 | {% include "admin/media_tree/filenode/includes/thumbnail_size_controls.html" %} 57 |
58 | {% endif %} 59 | {% if cl.has_filters %} 60 |

{% trans 'Filter' %}

61 | {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} 62 |
63 | {% endif %} 64 | {% endblock %} 65 | 66 | 67 | {% block result_list %} 68 | {% if list_type == 'stream' %} 69 | {% result_tree_flat cl request %} 70 | {% else %} 71 | {{ block.super }} 72 | {% endif %} 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Media Tree |latest-version| 2 | ********************************** 3 | 4 | |travis-ci| |coveralls| |health| |downloads| |license| 5 | 6 | Django Media Tree is a Django app for managing your website's media files in a 7 | folder tree, and using them in your own applications. 8 | 9 | .. |latest-version| image:: https://img.shields.io/pypi/v/django-media-tree.svg 10 | :alt: Latest version on PyPI 11 | :target: https://pypi.python.org/pypi/django-media-tree 12 | .. |travis-ci| image:: https://travis-ci.org/samluescher/django-media-tree.svg 13 | :alt: Build status 14 | :target: https://travis-ci.org/samluescher/django-media-tree 15 | .. |coveralls| image:: https://coveralls.io/repos/samluescher/django-media-tree/badge.svg 16 | :alt: Test coverage 17 | :target: https://coveralls.io/r/samluescher/django-media-tree 18 | .. |health| image:: https://landscape.io/github/samluescher/django-media-tree/master/landscape.svg?style=flat 19 | :target: https://landscape.io/github/samluescher/django-media-tree/master 20 | :alt: Code Health 21 | .. |downloads| image:: https://img.shields.io/pypi/dm/django-media-tree.svg 22 | :alt: Monthly downloads from PyPI 23 | :target: https://pypi.python.org/pypi/django-media-tree 24 | .. |license| image:: https://img.shields.io/pypi/l/django-media-tree.svg 25 | :alt: Software license 26 | :target: https://github.com/samluescher/django-media-tree/blob/master/LICENSE 27 | 28 | Key Features 29 | ============ 30 | 31 | * Enables you to organize all of your site media in nested folders. 32 | * Supports various media types (images, audio, video, archives etc). 33 | * Extension system, enabling you to easily add special processing for different 34 | media types and extend the admin interface. 35 | * Speedy AJAX-enhanced admin interface with drag & drop and dynamic resizing. 36 | * Upload queue with progress indicators (using Fine Uploader). 37 | * Add metadata to all media to improve accessibility of your web sites. 38 | * Integration with `Django CMS`_. Plugins include: image, slideshow, gallery, 39 | download list -- create your own! 40 | 41 | .. _Django CMS: http://www.django-cms.org/ 42 | 43 | Documentation 44 | ============= 45 | 46 | http://django-media-tree.readthedocs.org/ 47 | 48 | Development 49 | =========== 50 | 51 | Contributors should make sure the demo project builds successfully with their 52 | changes before placing a pull request on GitHub. This is best done by running 53 | the tests. 54 | 55 | * Either: ``python setup.py -q test`` (run ``tox`` against all supported versions) 56 | * Or: ``python setup.py test -a --skip-missing-interpreters`` (skip Python 57 | interpreters that are not available) 58 | * Or: ``python setup.py test -a "-e py27-django16"`` (only test the Python 2.7 59 | + Django 1.6 combination) 60 | 61 | It's also advisable to run ``flake8`` and address complaints before pushing 62 | changes to ensure code health increases. 63 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/templates/default.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 48 | 49 | Fine Uploader default UI 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_slideshow/models.py: -------------------------------------------------------------------------------- 1 | from media_tree.fields import DimensionField 2 | from media_tree.contrib.cms_plugins.media_tree_slideshow import settings as app_settings 3 | from media_tree.contrib.cms_plugins.media_tree_listing.models import MediaTreeListingBase, MediaTreeListingItemBase 4 | from media_tree.contrib.cms_plugins import settings as plugins_settings 5 | from media_tree.fields import FileNodeForeignKey 6 | from cms.models import Page 7 | from django.db import models 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | 11 | class MediaTreeImageListingBase(MediaTreeListingBase): 12 | timeout = models.IntegerField(_('pause between images'), default=4000, help_text=_('Milliseconds')) 13 | fx = models.CharField(_('transition'), max_length=32, default='fade', choices=app_settings.MEDIA_TREE_SLIDESHOW_TRANSITION_FX_CHOICES) 14 | speed = models.CharField(_('transition speed'), max_length=8, default='normal', choices=(('slow', _('slow')), ('normal', _('normal')), ('fast', _('fast')))) 15 | width = DimensionField(_('max. width'), null=True, blank=True, help_text=_('You can leave this empty to use an automatically determined image width.')) 16 | height = DimensionField(_('max. height'), null=True, blank=True, help_text=_('You can leave this empty to use an automatically determined image height.')) 17 | filter_supported = models.BooleanField(_('output supported media types only'), default=True) 18 | 19 | class Meta: 20 | abstract = True 21 | ordering = ['position', 'id'] 22 | verbose_name = _('media object') 23 | verbose_name_plural = _('media objects') 24 | 25 | 26 | class MediaTreeSlideshow(MediaTreeImageListingBase): 27 | pass 28 | 29 | 30 | class MediaTreeImageItemBase(MediaTreeListingItemBase): 31 | link_type = models.CharField(_('link type'), max_length=1, blank=True, null=True, default=plugins_settings.MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_DEFAULT, choices=plugins_settings.MEDIA_TREE_CMS_PLUGIN_LINK_TYPE_CHOICES, help_text=_('Makes the image a clickable link.')) 32 | link_page = models.ForeignKey(Page, verbose_name=_("page"), null=True, blank=True, help_text=_('For link to page. Select any page from the list.')) 33 | link_url = models.CharField(_("web address"), max_length=255, blank=True, null=True, help_text=_('For link to web address. Example: Enter "http://www.domain.com" to create an absolute link to an external site, or enter a relative URL like "/about/contact".')) 34 | link_target = models.CharField(_("link target"), max_length=64, blank=True, null=True, choices=(('', _('same window')), ('_blank', _('new window')))) 35 | 36 | class Meta: 37 | abstract = True 38 | ordering = ['position', 'id'] 39 | verbose_name = _('media object') 40 | verbose_name_plural = _('media objects') 41 | 42 | 43 | class MediaTreeSlideshowItem(MediaTreeImageItemBase): 44 | list_plugin = models.ForeignKey(MediaTreeSlideshow, related_name='media_items') 45 | node = FileNodeForeignKey(verbose_name=_('folder/file')) 46 | -------------------------------------------------------------------------------- /media_tree/contrib/media_extensions/images/focal_point/static/focal_point/js/focal_point.js: -------------------------------------------------------------------------------- 1 | jQuery(function($) { 2 | var thumb = $('.form-row.file .thumbnail, .form-row.field-file .thumbnail'); 3 | if (thumb.length) { 4 | 5 | var focalPoint = $(''); 6 | var img = $('img', thumb); 7 | var imgW = img.attr('width'); 8 | var imgH = img.attr('height'); 9 | var container = $(''); 10 | container.append(focalPoint); 11 | thumb.append(container); 12 | 13 | var focalPointW = focalPoint.outerWidth(); 14 | var focalPointH = focalPoint.outerHeight(); 15 | var inputX = $('#id_focal_x'); 16 | var inputY = $('#id_focal_y'); 17 | var defaultX = .5; 18 | var defaultY = .5; 19 | container.width(imgW + focalPointW); 20 | container.height(imgH + focalPointH); 21 | container.css('left', -focalPointW / 2); 22 | container.css('top', -focalPointH / 2); 23 | 24 | focalPoint.setPosition = function(x, y) { 25 | $(this).css('left', x * imgW); 26 | $(this).css('top', y * imgH); 27 | } 28 | 29 | focalPoint.getPosition = function(decimals) { 30 | var x = focalPoint.position().left / imgW; 31 | var y = focalPoint.position().top / imgH; 32 | x = Math.min(1, Math.max(0, x)); 33 | y = Math.min(1, Math.max(0, y)); 34 | if (decimals) { 35 | var r = Math.pow(10, decimals); 36 | x = Math.round(x * r) / r; 37 | y = Math.round(y * r) / r; 38 | } 39 | return {x: x, y: y}; 40 | } 41 | 42 | focalPoint.setPositionToInput = function() { 43 | var x = inputX.val().replace(/[^0-9\.]/g, ''); 44 | var y = inputY.val().replace(/[^0-9\.]/g, ''); 45 | var emptyX = x == ''; 46 | var emptyY = y == ''; 47 | if (emptyX) x = defaultX; else x = parseFloat(x); 48 | if (emptyY) y = defaultY; else y = parseFloat(y); 49 | x = Math.min(1, Math.max(0, x)); 50 | y = Math.min(1, Math.max(0, y)); 51 | if (!emptyX) inputX.val(x); 52 | if (!emptyY) inputY.val(y); 53 | focalPoint.setPosition(x, y); 54 | } 55 | 56 | focalPoint.draggable({ 57 | containment: 'parent' 58 | ,drag: function() { 59 | pos = focalPoint.getPosition(3); 60 | inputX.val(pos.x); 61 | inputY.val(pos.y); 62 | } 63 | }); 64 | 65 | inputX.change(focalPoint.setPositionToInput); 66 | inputY.change(focalPoint.setPositionToInput); 67 | 68 | focalPoint.setPositionToInput(); 69 | 70 | var fieldset = inputX.closest('fieldset'); 71 | if (fieldset.hasClass('collapsed')) { 72 | focalPoint.hide(); 73 | } 74 | 75 | fieldset.find('.collapse-toggle').click(function() { 76 | focalPoint.toggle(); 77 | }); 78 | } 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/templates/simple-thumbnails.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 50 | 51 | Fine Uploader default UI with thumbnails 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /media_tree/media_backends/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree import settings as app_settings 2 | from media_tree.utils import get_module_attr 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.conf import settings 5 | import os 6 | 7 | 8 | class ThumbnailError(Exception): 9 | pass 10 | 11 | def get_media_backend(fail_silently=True, handles_media_types=None, 12 | handles_file_extensions=None, supports_thumbnails=None): 13 | """ 14 | Returns the MediaBackend subclass that is configured for use with 15 | media_tree. 16 | """ 17 | backends = app_settings.MEDIA_TREE_MEDIA_BACKENDS 18 | if not len(backends): 19 | if not fail_silently: 20 | raise ImproperlyConfigured('There is no media backend configured.' \ 21 | + ' Please define `MEDIA_TREE_MEDIA_BACKENDS` in your settings.') 22 | else: 23 | return False 24 | 25 | for path in backends: 26 | # Traverse backends until there is one supporting what's requested: 27 | backend = get_module_attr(path) 28 | if (not handles_media_types or backend.handles_media_types(handles_media_types)) \ 29 | and (not handles_file_extensions or backend.handles_file_extensions(handles_file_extensions)) \ 30 | and (not supports_thumbnails or backend.supports_thumbnails()): 31 | return backend 32 | 33 | if not fail_silently: 34 | raise ImproperlyConfigured('There is no media backend configured to handle' \ 35 | ' the specified file types.') 36 | return False 37 | 38 | 39 | class MediaBackend: 40 | 41 | SUPPORTED_MEDIA_TYPES = None 42 | SUPPORTED_FILE_EXTENSIONS = None 43 | 44 | @classmethod 45 | def handles_media_types(cls, media_types): 46 | return cls.SUPPORTED_MEDIA_TYPES \ 47 | and len(set(media_types) - set(cls.SUPPORTED_MEDIA_TYPES)) == 0 48 | 49 | @classmethod 50 | def handles_file_extensions(cls, file_extensions): 51 | return cls.SUPPORTED_FILE_EXTENSIONS \ 52 | and len(set(file_extensions) - set(cls.SUPPORTED_FILE_EXTENSIONS)) == 0 53 | 54 | @staticmethod 55 | def supports_thumbnails(): 56 | return False 57 | 58 | @staticmethod 59 | def get_thumbnail(source, options): 60 | raise NotImplementedError('Media backends need to implement the `get_thumbnail()` method.') 61 | 62 | @staticmethod 63 | def get_valid_thumbnail_options(): 64 | raise NotImplementedError('Media backends need to implement the `get_valid_thumbnail_options()` method.') 65 | 66 | @staticmethod 67 | def get_cache_paths(subdirs=None): 68 | if not subdirs: 69 | raise NotImplementedError('Media backends need to implement the `get_cache_paths()` method.') 70 | paths = [] 71 | from media_tree.models import FileNode 72 | from django.db import models 73 | for field in FileNode._meta.fields: 74 | if isinstance(field, models.FileField) and hasattr(field, 'upload_to'): 75 | for subdir in subdirs: 76 | paths.append(os.path.join(field.upload_to, subdir)) 77 | return paths 78 | -------------------------------------------------------------------------------- /media_tree/admin/change_list.py: -------------------------------------------------------------------------------- 1 | import django 2 | from media_tree.models import FileNode 3 | from media_tree.admin.utils import get_current_request, is_search_request, \ 4 | get_request_attr 5 | from django.contrib.admin.views.main import ChangeList 6 | from django.db import models 7 | 8 | 9 | class MediaTreeChangeList(ChangeList): 10 | 11 | def is_filtered(self, request): 12 | return is_search_request(request) or self.params 13 | 14 | def __init__(self, request, *args, **kwargs): 15 | super(MediaTreeChangeList, self).__init__(request, *args, **kwargs) 16 | # self.parent_folder is set in get_queryset() 17 | self.title = self.parent_folder.name if self.parent_folder else FileNode.get_top_node().name 18 | 19 | # TODO: Move filtering by open folders here 20 | def get_queryset(self, request=None): 21 | 22 | # request arg was added in django r16144 (after 1.3) 23 | if request is not None and django.VERSION >= (1, 4): 24 | qs = super(MPTTChangeList, self).get_queryset(request) 25 | else: 26 | qs = super(MPTTChangeList, self).get_queryset() 27 | request = get_current_request() 28 | 29 | # Pagination should be disabled by default, since it interferes 30 | # with expanded folders and might display them partially. 31 | # However, filtered results are presented as a flat list and 32 | # should be paginated. 33 | pagination_enabled = self.is_filtered(request) 34 | if not pagination_enabled: 35 | self.show_all = True 36 | 37 | # filter by currently expanded folders if list is not filtered by extension or media_type 38 | self.parent_folder = self.model_admin.get_parent_folder(request) 39 | if self.parent_folder and not pagination_enabled: 40 | if self.parent_folder.is_top_node(): 41 | expanded_folders_pk = self.model_admin.get_expanded_folders_pk(request) 42 | if expanded_folders_pk: 43 | qs = qs.filter(models.Q(parent=None) | models.Q(parent__pk__in=expanded_folders_pk)) 44 | else: 45 | qs = qs.filter(parent=None) 46 | else: 47 | qs = qs.filter(parent=self.parent_folder) 48 | 49 | if request is not None and self.is_filtered(request): 50 | return qs.order_by('name') 51 | else: 52 | # always order by (tree_id, left) 53 | tree_id = qs.model._mptt_meta.tree_id_attr 54 | left = qs.model._mptt_meta.left_attr 55 | return qs.order_by(tree_id, left) 56 | 57 | def get_results(self, request): 58 | """ 59 | Temporarily decreases the `level` attribute of all search results in 60 | order to prevent indendation when displaying them. 61 | """ 62 | super(MediaTreeChangeList, self).get_results(request) 63 | try: 64 | reduce_levels = abs(int(get_request_attr(request, 'reduce_levels', 0))) 65 | except TypeError: 66 | reduce_levels = 0 67 | is_filtered = self.is_filtered(request) 68 | if is_filtered or reduce_levels: 69 | for item in self.result_list: 70 | item.prevent_save() 71 | item.actual_level = item.level 72 | if is_filtered: 73 | item.reduce_levels = item.level 74 | item.level = 0 75 | else: 76 | item.reduce_levels = reduce_levels 77 | item.level = max(0, item.level - reduce_levels) 78 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/static/media_tree/js/jquery-inline-positioning.js: -------------------------------------------------------------------------------- 1 | /* 2 | Enables repositioning of all inline elements by drag & drop. 3 | 4 | The inline model requires is a "position" field that is blank by default. 5 | This value will be set automatically by this code snippet when dragging elements. 6 | The model instances can then be ordered by that "position" field. 7 | */ 8 | 9 | jQuery(function($) { 10 | 11 | var positionField = $.scriptUrlParam ? $.scriptUrlParam(/jquery-inline-positioning\.js(\?.*)?$/, 'positionField', 'position') : 'position'; 12 | var target = $('div.inline-group'); 13 | var handle = 'h3 b'; 14 | var item = 'div.inline-related'; 15 | var positionInput = 'input[id$=-'+positionField+']'; 16 | var hidePositionFieldClosest = '.form-row'; 17 | 18 | var renumberAll = function() { 19 | var pos = 1; 20 | target.find(item).each(function(i) { 21 | if ($(this).find(positionInput).val() != '') { 22 | $(this).find(positionInput).val(pos); 23 | pos++; 24 | } 25 | }); 26 | }; 27 | 28 | var init = function() { 29 | target.find(item).each(function(i) { 30 | if ($(this).data('isSortable')) return; 31 | $(this).data('isSortable', true); 32 | 33 | $(this).find(handle).css('cursor', 'move'); 34 | $(this).find(handle).addClass('draggable'); 35 | $(this).find(positionInput).each(function() { 36 | if (hidePositionFieldClosest) { 37 | var hidden =$(''); 38 | hidden.val($(this).val()); 39 | $(this).closest(hidePositionFieldClosest).replaceWith(hidden); 40 | } 41 | }); 42 | $(this).find('input, select, textarea').change(function() { 43 | $(this).closest(item).find('input[id$='+positionField+']').val('X'); // mark for renumberAll() to fill in 44 | renumberAll($('div.inline-group')); 45 | }); 46 | }); 47 | } 48 | 49 | var addRow = target.find('.add-row'); 50 | addRow.remove(); 51 | var ordered = []; 52 | var unordered = []; 53 | // Initially, remove and re-append all inlines ordered by their "position" value 54 | target.find(item).each(function(i) { 55 | var initialPos = $(this).find(positionInput).val(); 56 | if (initialPos) { 57 | while (initialPos < ordered.length && ordered[initialPos]) { 58 | initialPos++; 59 | } 60 | ordered[initialPos] = this; 61 | } else { 62 | unordered[unordered.length] = this; 63 | } 64 | this.parentElement.removeChild(this); 65 | }); 66 | for (var i = 0; i < ordered.length; i++) { 67 | var el = ordered[i]; 68 | if (el) { 69 | target.append(el); 70 | } 71 | } 72 | // Add "position"-less elements in the end 73 | for (var i = 0; i < unordered.length; i++) { 74 | var el = unordered[i]; 75 | target.append(el); 76 | } 77 | target.append(addRow); 78 | 79 | target.sortable({ 80 | containment: 'parent', 81 | /*zindex: 10, */ 82 | items: item, 83 | handle: handle, 84 | update: renumberAll, 85 | opacity: .75 86 | }); 87 | 88 | init(); 89 | // init again when "Add another" link is clicked 90 | $('.add-row a').click(function() { 91 | init(); 92 | }) 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /media_tree/forms.py: -------------------------------------------------------------------------------- 1 | from media_tree import settings as app_settings 2 | from django import forms 3 | from django.forms import ModelChoiceField 4 | from django.utils.translation import ugettext as _ 5 | from django.utils.encoding import smart_unicode 6 | 7 | LEVEL_INDICATOR = app_settings.MEDIA_TREE_LEVEL_INDICATOR 8 | 9 | 10 | class FileNodeChoiceField(ModelChoiceField): 11 | """ 12 | A form field for selecting a ``FileNode`` object. 13 | 14 | Its constructor takes the following arguments that are relevant when selecting ``FileNode`` objects: 15 | 16 | :param allowed_node_types: A list of node types that are allowed and will validate, e.g. ``(FileNode.FILE,)`` if the user should only be able to select files, but not folders 17 | :param allowed_media_types: A list of media types that are allowed and will validate, e.g. ``(media_types.DOCUMENT,)`` 18 | :param allowed_extensions: A list of file extensions that are allowed and will validate, e.g. ``("jpg", "jpeg")`` 19 | 20 | Since this class is a subclass of ``ModelChoiceField``, you can also pass it that class' 21 | parameters, such as ``queryset`` if you would like to restrict the objects that will 22 | be available for selection. 23 | """ 24 | 25 | def __init__(self, allowed_node_types=None, allowed_media_types=None, allowed_extensions=None, level_indicator=LEVEL_INDICATOR, rel=None, *args, **kwargs): 26 | self.allowed_node_types = allowed_node_types 27 | self.allowed_media_types = allowed_media_types 28 | self.allowed_extensions = allowed_extensions 29 | self.level_indicator = level_indicator 30 | super(FileNodeChoiceField, self).__init__(*args, **kwargs) 31 | 32 | def clean(self, value): 33 | result = super(FileNodeChoiceField, self).clean(value) 34 | errors = [] 35 | if result != None: 36 | if self.allowed_node_types and not result.node_type in self.allowed_node_types: 37 | if len(self.allowed_node_types) == 1 and FileNode.FILE in self.allowed_node_types: 38 | errors.append(_('Please select a file.')) 39 | elif len(self.allowed_node_types) == 1 and FileNode.FOLDER in self.allowed_node_types: 40 | errors.append(_('Please select a folder.')) 41 | else: 42 | errors.append(_('You cannot select this node type.')) 43 | if self.allowed_media_types and not result.media_type in self.allowed_media_types: 44 | if len(self.allowed_media_types) == 1: 45 | label = app_settings.MEDIA_TREE_CONTENT_TYPES[self.allowed_media_types[0]] 46 | errors.append(_('The required media type is %s.') % label) 47 | else: 48 | errors.append(_('You cannot select this media type.')) 49 | if self.allowed_extensions and not result.extension in self.allowed_extensions: 50 | if len(self.allowed_extensions) == 1: 51 | errors.append(_('The required file type is %s.') % self.allowed_extensions[0]) 52 | else: 53 | errors.append(_('You cannot select this file type.')) 54 | if len(errors) > 0: 55 | raise forms.ValidationError(errors) 56 | return result 57 | 58 | def label_from_instance(self, obj): 59 | """ 60 | Creates labels which represent the tree level of each node when 61 | generating option labels. 62 | """ 63 | return u'%s %s %i' % (self.level_indicator * (getattr(obj, 'depth') - 1), smart_unicode(obj), obj.depth) 64 | -------------------------------------------------------------------------------- /demo_project/demo_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo_project project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '-m%eb@zk0sb9lf%+k9w(i#f%hyfsyp_m6vp$wm=zok*fj$&d-4' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'mptt', 40 | 'easy_thumbnails', 41 | 'media_tree', 42 | 'media_tree.contrib.views' 43 | ) 44 | 45 | MIDDLEWARE_CLASSES = ( 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ) 53 | 54 | ROOT_URLCONF = 'demo_project.urls' 55 | 56 | WSGI_APPLICATION = 'demo_project.wsgi.application' 57 | 58 | 59 | # Database 60 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 61 | 62 | DATABASES = { 63 | 'default': { 64 | 'ENGINE': 'django.db.backends.sqlite3', 65 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 66 | } 67 | } 68 | 69 | # Internationalization 70 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 71 | 72 | LANGUAGE_CODE = 'en-us' 73 | 74 | TIME_ZONE = 'UTC' 75 | 76 | USE_I18N = True 77 | 78 | USE_L10N = True 79 | 80 | USE_TZ = True 81 | 82 | 83 | # Static files (CSS, JavaScript, Images) 84 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 85 | 86 | STATIC_URL = '/static/' 87 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 88 | 89 | MEDIA_TREE_MEDIA_BACKENDS = ( 90 | 'media_tree.contrib.media_backends.easy_thumbnails.EasyThumbnailsBackend', 91 | ) 92 | 93 | THUMBNAIL_SUBDIR = '_thumbs' 94 | 95 | # Absolute filesystem path to the directory that will hold user-uploaded files. 96 | # Example: "/home/media/media.lawrence.com/media/" 97 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 98 | 99 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 100 | # trailing slash. 101 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 102 | MEDIA_URL = '/media/' 103 | 104 | TEMPLATE_DIRS = ( 105 | os.path.join(BASE_DIR, 'templates'), 106 | ) 107 | 108 | # List of callables that know how to import templates from various sources. 109 | TEMPLATE_LOADERS = ( 110 | 'django.template.loaders.filesystem.Loader', 111 | 'django.template.loaders.app_directories.Loader', 112 | # 'django.template.loaders.eggs.Loader', 113 | ) 114 | 115 | FIXTURE_DIRS = ( 116 | os.path.join(BASE_DIR, 'fixtures'), 117 | ) 118 | -------------------------------------------------------------------------------- /media_tree/extension/base_extenders/model_extender.py: -------------------------------------------------------------------------------- 1 | from media_tree.extension.base_extenders import MediaTreeExtender 2 | from media_tree.models import FileNode 3 | from django.db.models import fields 4 | from django.db.models import signals 5 | from types import FunctionType 6 | 7 | 8 | class ModelExtender(MediaTreeExtender): 9 | """In many cases, extensions need to store additional data and add custom 10 | fields to the extended model class. This is achieved by defining model 11 | Fields in a ``ModelExtender`` subclass, just like they are defined in a 12 | Model class:: 13 | 14 | class MyModelExtender(extension.ModelExtender): 15 | some_field = models.CharField('a new field', max_length=10) 16 | 17 | All fields defined like this will be added to to the extended model class 18 | during runtime. 19 | 20 | .. note:: 21 | If your extender defines model Fields, you are going to have to add these 22 | to the database table yourself, since Media Tree does not make changes to 23 | the table. However, using ``syncdb`` after installing an extension will 24 | create the table including any new fields defined by extensions. 25 | """ 26 | 27 | SIGNAL_NAMES = ('pre_save', 'post_save', 'pre_delete', 'post_delete') 28 | """To perform custom processing and data manipulations when a Model instance 29 | is changed, you can use signals. 30 | 31 | For each signal name listed in this attribute, you can define an 32 | extender method that will be receiving the respective signal. By default, 33 | the allowed signals are: 34 | ``'pre_save', 'post_save', 'pre_delete', 'post_delete'``. 35 | 36 | For instance, if the extender should set a specific model Field before the 37 | model instance is saved, define the ``pre_save`` receiver:: 38 | 39 | class MyModelExtender(extension.ModelExtender): 40 | 41 | SIGNAL_NAMES = ('pre_save',) 42 | 43 | @staticmethod 44 | def pre_save(sender, **kwargs): 45 | sender.some_field = 'some_value' 46 | 47 | .. note:: 48 | If the signal receiver function is a class member, it needs to be a **static 49 | method** (hence the ``@staticmethod`` decorator in the example above), 50 | as it won't be passed an instance of the class it belongs to. Also, the 51 | function takes a ``sender`` argument, along with wildcard keyword 52 | arguments (``**kwargs``). You should refer to the `Django Signals 53 | documentation `_ 54 | if you are not familiar with this topic. 55 | """ 56 | 57 | @classmethod 58 | def contribute(extender, extended_class=FileNode): 59 | for attr_name, attr in vars(extender).items(): 60 | if issubclass(attr.__class__, fields.Field): 61 | # Add all fields defined in extender to models 62 | attr.contribute_to_class(extended_class, attr_name) 63 | elif issubclass(attr.__class__, FunctionType): 64 | if attr_name in extender.SIGNAL_NAMES: 65 | # Connect signals defined in extender. 66 | # Note that if the extender is a class, the receiver methods must be static, 67 | # so you should use the @staticmethod decorator 68 | sender = getattr(signals, attr_name) 69 | receiver = getattr(extender, attr_name) 70 | sender.connect(receiver, sender=extended_class) 71 | else: 72 | # Add all other functions to model 73 | setattr(extended_class, attr_name, attr) 74 | -------------------------------------------------------------------------------- /media_tree/templates/admin/media_tree/filenode/actions_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n staticfiles %} 3 | {% block breadcrumbs %} 4 | {% with title as breadcrumbs_title %} 5 | {% include "admin/media_tree/filenode/includes/breadcrumbs.html" %} 6 | {% endwith %} 7 | {% endblock %} 8 | {% block extrahead %} 9 | {{ block.super }} 10 | {{ form.media }} 11 | {% endblock %} 12 | {% block extrastyle %}{{ block.super }}{% endblock %} 13 | {% block content %} 14 | {% if select_all %} 15 | 33 | {% endif %} 34 | {% if form %} 35 |
36 | {% if form.errors %} 37 |
    38 |
  • {% blocktrans count form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
  • 39 |
40 | {% endif %} 41 | {% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %} 42 | 43 | {% if select_all %} 44 |

  45 |

46 | {% endif %} 47 | 48 |
49 | {% for field in form %} 50 | {% if field.is_hidden %} 51 | {{ field }} 52 | {% else %} 53 |
54 | {{ field.errors }} 55 |
56 | 57 | {{ field }} 58 | {% if form.confirm_fields and field.name in form.confirm_fields %} 59 | 69 | {% endif %} 70 |
71 | {% if field.help_text %}

{{ field.help_text|linebreaksbr }}

{% endif %} 72 |
73 | {% endif %} 74 | {% endfor %} 75 |
76 | 77 | {% block preview %} 78 | {% endblock %} 79 |
80 | {% block submit %} 81 | 82 | {% endblock %} 83 |
84 | {% csrf_token %} 85 |
86 | {% endif %} 87 | {% block selected %} 88 | {% if node_list %} 89 |

{% if not node_list_title %}{% trans "Selected media objects:" %}{% else %}{{ node_list_title }}{% endif %}

90 |
    91 | {{ node_list|unordered_list }} 92 |
93 | {% endif %} 94 | {% endblock %} 95 | {% endblock %} 96 | -------------------------------------------------------------------------------- /demo_project/templates/media_tree/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to Django Media Tree 5 | 61 | 62 | 63 | 64 |
65 |

Django Media Tree Demo Project

66 |

It worked! Welcome to a quick demonstration of Django Media Tree.

67 |
68 | 69 |
70 |

Administration

71 |

72 | Please log into the administration site to see how FileNode objects are administrated. 73 |

74 |

75 | Username: admin, Password: admin
76 |

77 | 80 |

Documentation

81 |
    82 |
  • 83 | The Media Tree Documentation contains extensive information on how to use and extend this application. 84 |
  • 85 |
86 | 87 |

Generic Views

88 |

89 | This application includes class-based generic views that enable you to access FileNode objects through public URLs. 90 | Please see the Demo Project's urls.py for an example of how to use them. 91 | Of course you can also extend the generic view classes to create views that suit your specific requirements. 92 |

93 | 98 |
99 | 100 |
101 |
102 | {% block content %}Please select one of the example views above.{% endblock %} 103 |
104 |
105 | 106 | -------------------------------------------------------------------------------- /media_tree/static/media_tree/lib/jquery.fineuploader-4.4.0/fineuploader-4.4.0.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Fine Uploader 3 | * 4 | * Copyright 2013, Widen Enterprises, Inc. info@fineuploader.com 5 | * 6 | * Version: 4.4.0 7 | * 8 | * Homepage: http://fineuploader.com 9 | * 10 | * Repository: git://github.com/Widen/fine-uploader.git 11 | * 12 | * Licensed under GNU GPL v3, see LICENSE 13 | */ 14 | 15 | 16 | /*! fineuploader 2014-03-15 */ 17 | 18 | .qq-uploader{position:relative;width:100%}.qq-upload-button{display:block;width:105px;padding:7px 0;text-align:center;background:#800;border-bottom:1px solid #DDD;color:#FFF}.qq-upload-button-hover{background:#C00}.qq-upload-button-focus{outline:1px dotted #000}.qq-upload-drop-area,.qq-upload-extra-drop-area{position:absolute;top:0;left:0;width:100%;height:100%;min-height:30px;z-index:2;background:#FF9797;text-align:center}.qq-upload-drop-area span{display:block;position:absolute;top:50%;width:100%;margin-top:-8px;font-size:16px}.qq-upload-extra-drop-area{position:relative;margin-top:50px;font-size:16px;padding-top:30px;height:20px;min-height:40px}.qq-upload-drop-area-active{background:#FF7171}.qq-upload-list{margin:0;padding:0;list-style:none}.qq-upload-list li{margin:0;padding:9px;line-height:15px;font-size:16px;background-color:#FFF0BD}.qq-upload-file,.qq-upload-spinner,.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-failed-text,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{margin-right:12px;display:inline}.qq-upload-file{}.qq-upload-spinner{display:inline-block;background:url(loading.gif);width:15px;height:15px;vertical-align:text-bottom}.qq-drop-processing{display:block}.qq-drop-processing-spinner{display:inline-block;background:url(processing.gif);width:24px;height:24px;vertical-align:text-bottom}.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{display:inline}.qq-upload-retry,.qq-upload-delete,.qq-upload-cancel,.qq-upload-pause,.qq-upload-continue{color:#000}.qq-upload-retryable .qq-upload-retry{display:inline}.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{font-size:12px;font-weight:400}.qq-upload-failed-text{display:none;font-style:italic;font-weight:700}.qq-upload-failed-icon{display:none;width:15px;height:15px;vertical-align:text-bottom}.qq-upload-fail .qq-upload-failed-text{display:inline}.qq-upload-retrying .qq-upload-failed-text{display:inline;color:#D60000}.qq-upload-list li.qq-upload-success{background-color:#5DA30C;color:#FFF}.qq-upload-list li.qq-upload-fail{background-color:#D60000;color:#FFF}.qq-progress-bar{display:block;background:-moz-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,rgba(30,87,153,1)),color-stop(50%,rgba(41,137,216,1)),color-stop(51%,rgba(32,124,202,1)),color-stop(100%,rgba(125,185,232,1)));background:-webkit-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-o-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-ms-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:linear-gradient(to bottom,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);width:0;height:15px;border-radius:6px;margin-bottom:3px}.qq-total-progress-bar{height:25px;border-radius:9px}.qq-total-progress-bar-container{margin:9px}INPUT.qq-edit-filename{position:absolute;opacity:0;filter:alpha(opacity=0);z-index:-1;-ms-filter:"alpha(Opacity=0)"}.qq-upload-file.qq-editable{cursor:pointer}.qq-edit-filename-icon.qq-editable{display:inline-block;cursor:pointer}INPUT.qq-edit-filename.qq-editing{position:static;margin-top:-5px;margin-right:10px;margin-bottom:-5px;opacity:1;filter:alpha(opacity=100);-ms-filter:"alpha(Opacity=100)"}.qq-edit-filename-icon{display:none;background:url(edit.gif);width:15px;height:15px;vertical-align:text-bottom;margin-right:5px}INPUT.qq-edit-filename.qq-editing~.qq-upload-cancel{display:none}.qq-hide{display:none} 19 | /*! 2014-03-15 */ 20 | -------------------------------------------------------------------------------- /media_tree/contrib/cms_plugins/media_tree_gallery/templates/cms/plugins/media_tree_gallery.html: -------------------------------------------------------------------------------- 1 | {% load i18n media_tree_thumbnail sekizai_tags%} 2 | {% addtoblock "js" %}{% endaddtoblock %} 3 |
4 | 5 | {% if folder_list and folder_list|length > 1 %} 6 | {{ folder_list|length }} 7 |
    8 | {{ folder_list|unordered_list }} 9 |
10 | {% endif %} 11 | 12 | {% if node_list %} 13 | 14 |
15 | {% for image_node in node_list %} 16 | {% include "cms/plugins/media_tree_image.html" %} 17 | {% endfor %} 18 |
19 | 20 | {% ifnotequal node_list|length 1 %} 21 | 30 | {% endifnotequal %} 31 | 32 | {% addtoblock "js" %} 33 | 94 | {% endaddtoblock %} 95 | 96 | {% endif %} 97 | 98 |
99 | -------------------------------------------------------------------------------- /docs/custom_plugins.rst: -------------------------------------------------------------------------------- 1 | Creating custom plugins for use with 3rd-party applications 2 | *********************************************************** 3 | 4 | .. _custom-plugins-howto: 5 | 6 | How to create custom plugins 7 | ============================ 8 | 9 | Django Media Tree comes with some generic View classes and Mixins that make it 10 | relatively easy to use ``FileNode`` objects with your own applications. 11 | 12 | The following pseudo code should give you an idea of how to implement your own 13 | custom plugin that will render a file listing and work together with the 14 | 3rd-party application of your choice. It loosely looks like a Django CMS plugin. 15 | Please notice that the ``render()`` method is passed an 16 | ``options_instance``, which can be a dictionary or an object with 17 | attributes to initialize the generic View class we are using, which is 18 | ``FileNodeListingView`` in this case. See :ref:`generic-views` for more 19 | information on the View classes themselves:: 20 | 21 | 22 | from media_tree.contrib.views.listing import FileNodeListingMixin 23 | from third_party_app import YourPluginSuperclass 24 | from django.shortcuts import render_to_response 25 | 26 | # Notice we are subclassing our third-party plugin class, 27 | # as well as the FileNodeListingMixin 28 | class CustomFileNodeListingPlugin(YourPluginSuperclass, FileNodeListingMixin): 29 | 30 | # Assuming render() is a standard method of YourPluginSuperclass 31 | def render(self, request, options_instance): 32 | 33 | # Get the generic view class using the method inherited from 34 | # the Mixin class. 35 | # Notice that get_detail_view() is inherited from the 36 | # FileNodeListingMixin. We are also passing our options model 37 | # instance for configuring the view instance. 38 | view = self.get_detail_view(request, 39 | queryset=options_instance.selected_folders, 40 | opts=options_instance) 41 | 42 | # Get the template context as generated by the View class 43 | context_data = view.get_context_data() 44 | 45 | # Render with custom template 46 | return render_to_response('listing.html', context_data) 47 | 48 | This is what our model classes (namely the class of the ``options_instance`` above) 49 | might look like:: 50 | 51 | from django.db import Models 52 | from media_tree.fields import FileNodeForeignKey 53 | 54 | class PluginOptions(models.Model): 55 | # These field names are derived from 56 | # media_tree.contrib.views.list.FileNodeListingView. 57 | list_max_depth = models.IntegerField() 58 | include_descendants = models.BooleanField() 59 | 60 | class SelectedFolder(models.Model): 61 | plugin = models.ForeignKey(PluginOptions) 62 | folder = FileNodeForeignKey() 63 | 64 | The first class contains our plugin option fields. Notice that when calling the 65 | ``get_detail_view()`` or ``get_view()`` methods provided by the 66 | ``FileNodeListingMixin`` and passing it an instance of this model, any fields that 67 | match attributes of the view object returned will be used to initialized the 68 | view object. 69 | 70 | The second class creates a relationship between the options model and the 71 | ``FileNode`` model, i.e. you will be able to link ``FileNode`` objects to 72 | plugins. 73 | 74 | 75 | View Mixins 76 | =========== 77 | 78 | View Mixins are classes that add methods useful for interfacing with 79 | Media Tree's generic view classes to your custom plugin classes, as 80 | demonstrated in the above example. 81 | 82 | .. automodule:: media_tree.contrib.views.mixin_base 83 | 84 | .. autoclass:: media_tree.contrib.views.listing.FileNodeListingMixin 85 | :members: 86 | :inherited-members: 87 | :exclude-members: get_view 88 | 89 | .. autoclass:: media_tree.contrib.views.detail.FileNodeDetailMixin 90 | :members: 91 | :inherited-members: 92 | :exclude-members: get_view 93 | 94 | .. autoclass:: media_tree.contrib.views.detail.image.ImageNodeDetailMixin 95 | :members: 96 | :inherited-members: 97 | :exclude-members: get_view 98 | 99 | -------------------------------------------------------------------------------- /media_tree/contrib/views/detail/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree.models import FileNode 2 | from media_tree.contrib.views.mixin_base import PluginMixin 3 | from django.views.generic.detail import DetailView 4 | from django.http import Http404 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class FileNodeDetailView(DetailView): 9 | """ 10 | View class for implementing detail views for ``FileNode`` objects. This 11 | class is based on Django's generic ``DetailView``. Please refer to the 12 | respective Django documentation on subclassing and customizing `Class-based 13 | generic views 14 | `_. 15 | 16 | The following example ``urls.py`` would implement a detail view capable of 17 | displaying all files with the extension ``.txt``, with URLs containing the 18 | full path of the ``FileNode`` object, for instance 19 | ``http://yourdomain.com/files/some/folder/ReadMe.txt``:: 20 | 21 | from media_tree.models import FileNode 22 | from media_tree.contrib.views.detail import FileNodeDetailView 23 | from django.conf.urls.defaults import * 24 | 25 | urlpatterns = patterns('', 26 | (r'^files/(?P.+)/$', FileNodeDetailView.as_view( 27 | queryset=FileNode.objects.filter(extension='txt') 28 | )), 29 | ) 30 | """ 31 | 32 | model = FileNode 33 | 34 | filter_media_types = None 35 | """ An iterable containing media types to filter for. """ 36 | 37 | filter_node_types = (FileNode.FILE,) 38 | """ 39 | An iterable containing node types to filter for. By default this is 40 | ``(FileNode.FILE,)`` 41 | """ 42 | 43 | context_object_name = 'node' 44 | """ Designates the name of the variable to use in the context. """ 45 | 46 | template_name = "media_tree/filenode_detail.html" 47 | """ Name of the template. """ 48 | 49 | def get_object(self, queryset=None): 50 | """ 51 | Returns the object the view is displaying. 52 | 53 | By default this requires `self.queryset` and a `pk` or `path` argument 54 | in the URLconf, but subclasses can override this to return any object. 55 | """ 56 | # Use a custom queryset if provided; this is required for subclasses 57 | # like DateDetailView 58 | if queryset is None: 59 | queryset = self.get_queryset() 60 | 61 | path = self.kwargs.get('path', None) 62 | 63 | # Next, try looking up by path. 64 | if path is not None: 65 | queryset = queryset.filter(**FileNode.objects.get_filter_args_with_path( 66 | for_self=True, path=path)) 67 | try: 68 | obj = queryset.get() 69 | except FileNode.DoesNotExist: 70 | raise Http404(_(u"No %(verbose_name)s found matching the query") % 71 | {'verbose_name': queryset.model._meta.verbose_name}) 72 | return obj 73 | 74 | return super(FileNodeDetailView, self).get_object(queryset) 75 | 76 | def get_queryset(self, *args, **kwargs): 77 | queryset = super(FileNodeDetailView, self).get_queryset(*args, **kwargs) 78 | kwargs = {} 79 | if self.filter_node_types: 80 | kwargs['node_type__in'] = self.filter_node_types 81 | if self.filter_media_types: 82 | kwargs['media_type__in'] = self.filter_media_types 83 | return queryset.filter(**kwargs) 84 | 85 | def get_context_data(self, **kwargs): 86 | context = super(FileNodeDetailView, self).get_context_data(**kwargs) 87 | if not 'title' in context: 88 | context['title'] = context[self.context_object_name].title or context[self.context_object_name].name 89 | return context 90 | 91 | 92 | class FileNodeDetailMixin(PluginMixin): 93 | 94 | view_class = FileNodeDetailView 95 | """ The view class instantiated by ``get_detail_view()``. """ 96 | 97 | def get_detail_view(self, request, object, opts=None): 98 | """ 99 | Instantiates and returns the view class that will generate the actual 100 | context for this plugin. 101 | """ 102 | view = self.get_view(request, self.view_class, opts) 103 | view.object = object 104 | return view 105 | -------------------------------------------------------------------------------- /media_tree/widgets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | from django.contrib.admin.widgets import AdminFileWidget 4 | from django.contrib.admin.templatetags.admin_static import static 5 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget 6 | from django.utils.html import escape 7 | from django.template.loader import render_to_string 8 | from django.utils.safestring import mark_safe 9 | from django.utils.translation import ugettext as _ 10 | from media_tree import settings as app_settings 11 | from media_tree import media_types 12 | from media_tree.media_backends import get_media_backend, ThumbnailError 13 | 14 | THUMBNAIL_EXTENSIONS = app_settings.MEDIA_TREE_THUMBNAIL_EXTENSIONS 15 | THUMBNAIL_SIZE = app_settings.MEDIA_TREE_THUMBNAIL_SIZES['large'] 16 | STATIC_SUBDIR = app_settings.MEDIA_TREE_STATIC_SUBDIR 17 | 18 | 19 | class FileNodeForeignKeyRawIdWidget(ForeignKeyRawIdWidget): 20 | 21 | class Media: 22 | js = ( 23 | os.path.join(STATIC_SUBDIR, 'js', 'filenode_foreignkeywidget.js').replace("\\","/"), 24 | ) 25 | css = { 26 | 'all': ( 27 | os.path.join(STATIC_SUBDIR, 'css', 'filenode_preview.css').replace("\\","/"), 28 | ) 29 | } 30 | 31 | # the actual input field should be hidden... 32 | input_type = 'hidden' 33 | 34 | # ...but for the field to show up amongst the form's visible fields, return 35 | # False here. 36 | @property 37 | def is_hidden(self): 38 | return False 39 | 40 | def render(self, name, value, attrs=None): 41 | # insert a placeholder for widget preview if value is None so that 42 | # the dismissRelatedLookupPopup hook in Javascript can populate it. 43 | output = super(FileNodeForeignKeyRawIdWidget, self).render(name, value, attrs) 44 | extra = '' 45 | if value: 46 | extra = '%s' % ( 47 | static('admin/img/icon-deletelink.svg' if django.VERSION >= (1.9) else 'admin/img/icon-deletelink.svg'), _('Clear')) 48 | return mark_safe('%s%s%s' % 49 | (output, self.label_for_value(None) if not value else '', extra)) 50 | 51 | def label_for_value(self, value): 52 | # instead of just outputting the node's name, render the node (or None) 53 | # through widget_preview.html, which adds icons and thumbnails depending 54 | # on your configuration. 55 | key = self.rel.get_related_field().name 56 | try: 57 | if value: 58 | obj = self.rel.to._default_manager.using(self.db).get(**{key: value}) 59 | else: 60 | obj = None 61 | preview = render_to_string('admin/media_tree/filenode/includes/widget_preview.html', 62 | {'node': obj, 'preview_file': obj.get_preview_file() if obj else None}) 63 | 64 | return preview 65 | except (ValueError, self.rel.to.DoesNotExist): 66 | return '' 67 | 68 | 69 | class AdminThumbWidget(AdminFileWidget): 70 | """ 71 | An Image FileField Widget that shows a thumbnail if it has one. 72 | """ 73 | def __init__(self, attrs={}): 74 | super(AdminThumbWidget, self).__init__(attrs) 75 | 76 | def get_media_backend(self, media_type): 77 | return get_media_backend(fail_silently=True, handles_media_types=( 78 | media_type,)) 79 | 80 | def render(self, name, value, attrs=None): 81 | output = super(AdminThumbWidget, self).render(name, value, attrs) 82 | thumb = None 83 | 84 | if value and hasattr(value, "url"): 85 | media_backend = self.get_media_backend(media_types.SUPPORTED_IMAGE) 86 | if media_backend: 87 | try: 88 | thumb = media_backend.get_thumbnail(value, {'size': THUMBNAIL_SIZE}) 89 | except ThumbnailError: 90 | pass 91 | if not thumb: 92 | media_backend = self.get_media_backend(media_types.VECTOR_IMAGE) 93 | if media_backend: 94 | try: 95 | thumb = media_backend.get_thumbnail(value, {'size': THUMBNAIL_SIZE}) 96 | except ThumbnailError: 97 | pass 98 | if thumb: 99 | thumb_html = u'%s' % (thumb.url, os.path.basename(value.name), thumb.width, thumb.height) 100 | output = u'

%s

%s

' % (thumb_html, output) 101 | 102 | return mark_safe(output) 103 | -------------------------------------------------------------------------------- /media_tree/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from media_tree import settings as app_settings 2 | from django.conf import settings 3 | try: 4 | # Python 1.7 5 | from importlib import import_module 6 | except ImportError: 7 | from django.utils.importlib import import_module 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.core.files.storage import get_storage_class 10 | from django.utils.html import conditional_escape 11 | import re 12 | 13 | 14 | RE_SPLITEXT = re.compile('^(\.*.*?)((\.[^\.]+)*|\.)$') 15 | 16 | 17 | def get_media_storage(): 18 | klass = get_storage_class(import_path=app_settings.MEDIA_TREE_STORAGE) 19 | return klass() 20 | 21 | 22 | # TODO: This function should probably cache all imported modules 23 | def get_module_attr(path): 24 | i = path.rfind('.') 25 | module_name, attr_name = path[:i], path[i+1:] 26 | try: 27 | module = import_module(module_name) 28 | except ImportError, e: 29 | raise ImproperlyConfigured('Error importing module %s: "%s"' % (module_name, e)) 30 | try: 31 | attr = getattr(module, attr_name) 32 | except AttributeError: 33 | raise ImproperlyConfigured('Module "%s" does not define a "%s" callable' % (module_name, attr_name)) 34 | return attr 35 | 36 | 37 | def autodiscover_media_extensions(): 38 | """ 39 | Auto-discover INSTALLED_APPS media_extensions.py modules and fail silently when 40 | not present. This forces an import on them to register any media extension bits 41 | they may want. 42 | 43 | Rip of django.contrib.admin.autodiscover() 44 | """ 45 | import copy 46 | from django.conf import settings 47 | from django.utils.module_loading import module_has_submodule 48 | 49 | for app in settings.INSTALLED_APPS: 50 | mod = import_module(app) 51 | try: 52 | import_module('%s.media_extension' % app) 53 | except: 54 | if module_has_submodule(mod, 'media_extension'): 55 | raise 56 | 57 | 58 | def multi_splitext(basename): 59 | """ 60 | Similar to os.path.slittext(), but with special handling for files with multiple extensions, 61 | such as "archive.tar.gz": Returns a list containg three elements, the first being the 62 | name without any extensions (taking into account hidden files/leading periods), 63 | the second being the "full" extension, the third being the extension as returned by 64 | os.path.splitext. 65 | 66 | Examples: 67 | 68 | os.path.join('foo.bar') # => ('foo', '.bar') 69 | multi_splitext('foo.bar') # => ['foo', '.bar', '.bar'] 70 | 71 | os.path.join('foo.tar.gz') # => ('foo.tar', '.gz') 72 | multi_splitext('foo.tar.gz') # => ['foo', '.tar.gz', '.gz'] 73 | 74 | os.path.join('.foo.tar.gz') # => ('.foo.tar', '.gz') 75 | multi_splitext('.foo.tar.gz') # => ['.foo', '.tar.gz', '.gz'] 76 | 77 | os.path.join('.htaccess') # => ('.htaccess', '') 78 | multi_splitext('.htaccess') # => ['.htaccess', '', ''] 79 | 80 | os.path.join('.foo.bar.') # => ('.foo.bar', '.') 81 | multi_splitext('.foo.bar.') # => ['.foo.bar', '.', '.'] 82 | 83 | """ 84 | groups = list(RE_SPLITEXT.match(basename).groups()) 85 | if not groups[2]: 86 | groups[2] = groups[1] 87 | return groups 88 | 89 | 90 | def join_formatted(text, new_text, glue_format_if_true = u'%s%s', glue_format_if_false = u'%s%s', condition=None, format = u'%s', escape=False): 91 | """ 92 | Joins two strings, optionally escaping the second, and using one of two 93 | string formats for glueing them together, depending on whether a condition 94 | is True or False. 95 | 96 | This function is a shorthand for complicated code blocks when you want to 97 | format some strings and link them together. A typical use case might be: 98 | Wrap string B with tags, but only if it is not empty, and join it 99 | with A with a comma in between, but only if A is not empty, etc. 100 | """ 101 | if condition is None: 102 | condition = text and new_text 103 | add_text = new_text 104 | if escape: 105 | add_text = conditional_escape(add_text) 106 | if add_text: 107 | add_text = format % add_text 108 | glue_format = glue_format_if_true if condition else glue_format_if_false 109 | return glue_format % (text, add_text) 110 | 111 | 112 | # TODO: Factor out to image extension 113 | def widthratio(value, max_value, max_width): 114 | """ 115 | Does the same like Django's `widthratio` template tag (scales max_width to factor value/max_value) 116 | """ 117 | ratio = float(value) / float(max_value) 118 | return int(round(ratio * max_width)) 119 | -------------------------------------------------------------------------------- /media_tree/fields.py: -------------------------------------------------------------------------------- 1 | from media_tree import settings as media_types 2 | from media_tree import media_types 3 | from media_tree.models import FileNode 4 | from media_tree.widgets import FileNodeForeignKeyRawIdWidget 5 | from media_tree.forms import FileNodeChoiceField, LEVEL_INDICATOR 6 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget 7 | from django.db import models 8 | from django import forms 9 | from django.conf import settings 10 | 11 | class FileNodeForeignKey(models.ForeignKey): 12 | """ 13 | A model field for selecting a ``FileNode`` object. 14 | 15 | Its constructor takes the following arguments that are relevant when selecting ``FileNode`` objects: 16 | 17 | :param allowed_node_types: A list of node types that are allowed and will validate, e.g. ``(FileNode.FILE,)`` if the user should only be able to select files, but not folders 18 | :param allowed_media_types: A list of media types that are allowed and will validate, e.g. ``(media_types.DOCUMENT,)`` 19 | :param allowed_extensions: A list of file extensions that are allowed and will validate, e.g. ``("jpg", "jpeg")`` 20 | 21 | Since this class is a subclass of ``models.ForeignKey``, you can also pass it that class' 22 | parameters, such as ``limit_choices_to`` if you would like to restrict the objects that will 23 | be available for selection. 24 | """ 25 | 26 | def __init__(self, allowed_node_types=None, allowed_media_types=None, allowed_extensions=None, level_indicator=LEVEL_INDICATOR, *args, **kwargs): 27 | self.allowed_node_types = allowed_node_types 28 | self.allowed_media_types = allowed_media_types 29 | self.allowed_extensions = allowed_extensions 30 | self.level_indicator = level_indicator 31 | kwargs['to'] = 'media_tree.FileNode' 32 | super(FileNodeForeignKey, self).__init__(*args, **kwargs) 33 | 34 | def formfield(self, **kwargs): 35 | defaults = { 36 | 'form_class': FileNodeChoiceField, 37 | 'rel': self.rel, 38 | 'allowed_node_types': self.allowed_node_types, 39 | 'allowed_media_types': self.allowed_media_types, 40 | 'allowed_extensions': self.allowed_extensions, 41 | 'empty_label': '', 42 | } 43 | 44 | defaults.update(kwargs) 45 | field = super(FileNodeForeignKey, self).formfield(**defaults) 46 | 47 | # If the widget is a ForeignKeyRawIdWidget, overwrite it with 48 | # FileNodeForeignKeyRawIdWidget. This is done here since the 49 | # widget's contructor parameters are coming from the ModelAdmin, 50 | # which we have no direct access to here. 51 | if isinstance(field.widget, ForeignKeyRawIdWidget) and not \ 52 | isinstance(field.widget, FileNodeForeignKeyRawIdWidget): 53 | field.widget = FileNodeForeignKeyRawIdWidget(field.widget.rel, 54 | field.widget.admin_site, using=field.widget.db) 55 | return field 56 | 57 | def south_field_triple(self): 58 | "Returns a suitable description of this field for South." 59 | from south.modelsinspector import introspector 60 | field_class = "django.db.models.fields.related.ForeignKey" 61 | args, kwargs = introspector(self) 62 | return (field_class, args, kwargs) 63 | 64 | 65 | class ImageFileNodeForeignKey(FileNodeForeignKey): 66 | """ 67 | A model field for selecting a ``FileNode`` object associated to a supported image format. 68 | 69 | Using this field will ensure that only folders and image files will be visible in the widget, 70 | and will require the user to select an image node. 71 | """ 72 | def __init__(self, allowed_node_types=None, allowed_media_types=None, allowed_extensions=None, level_indicator=LEVEL_INDICATOR, *args, **kwargs): 73 | self.allowed_node_types = allowed_node_types 74 | if not allowed_media_types: 75 | allowed_media_types = (media_types.SUPPORTED_IMAGE,) 76 | kwargs['limit_choices_to'] = {'media_type__in': (FileNode.FOLDER, media_types.SUPPORTED_IMAGE)} 77 | 78 | super(ImageFileNodeForeignKey, self).__init__(allowed_node_types, allowed_media_types, allowed_extensions, level_indicator, *args, **kwargs) 79 | 80 | 81 | class DimensionField(models.CharField): 82 | """ 83 | CharField for specifying image dimensions, i.e. width or height. Currently, 84 | this needs to be an integer > 0, but since it is a CharField, it might also 85 | contain units such as "px" or "%" in the future. 86 | """ 87 | def __init__(self, verbose_name=None, name=None, **kwargs): 88 | if not 'max_length' in kwargs: 89 | kwargs['max_length'] = 8 90 | super(DimensionField, self).__init__(verbose_name, name, **kwargs) 91 | 92 | def formfield(self, **kwargs): 93 | defaults = {'regex': '^[1-9][0-9]*$', 94 | 'form_class': forms.RegexField} 95 | defaults.update(kwargs) 96 | return models.Field.formfield(self, **defaults) 97 | 98 | def south_field_triple(self): 99 | "Returns a suitable description of this field for South." 100 | from south.modelsinspector import introspector 101 | field_class = "django.db.models.fields.CharField" 102 | args, kwargs = introspector(self) 103 | return (field_class, args, kwargs) 104 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuring Media Tree 4 | ********************** 5 | 6 | The following settings can be specified in your Django project's settings 7 | module. 8 | 9 | 10 | ``MEDIA_TREE_STORAGE`` 11 | File storage class to be used for any file-related operations when dealing 12 | with media files. 13 | 14 | This is not set by default, meaning that Django's ``DEFAULT_FILE_STORAGE`` 15 | will be used. If you need to implement your custom storage, please refer to the 16 | relevant Django `documentation on that setting 17 | `_ and 18 | on `file storage 19 | `_ 20 | in general. 21 | 22 | ``MEDIA_TREE_DELETE_FROM_STORAGE_ON_OVERWRITE`` 23 | 24 | Determines whether existing files should be deleted from storage when the 25 | corresponding object is overwritten with a new file. This prevents the creation 26 | of orphaned files. As an added benefit in the case of Django's 27 | `FileSystemStorage`, if this setting is `True` and a file is supposed to be 28 | overwritten with a new file that has the same name, that name remains permanent 29 | because the old file will be deleted before the object is saved. If `False`, a 30 | random hash would be added to the new file's name due to the existing file 31 | already occupying the desired name on disk. 32 | """ 33 | 34 | Default: ``True`` 35 | 36 | ``MEDIA_TREE_MEDIA_BACKENDS`` 37 | A tuple of media backends for thumbnail generation and other media-related 38 | tasks, i.e. a list of wrappers for the 3rd-party applications that take 39 | care of them. 40 | 41 | .. Note:: 42 | Please refer to the :ref:`installation instructions 43 | ` for information on how to configure 44 | supported media backends. 45 | 46 | For general information on media backends, see :ref:`media-backends` for 47 | more information. 48 | 49 | 50 | ``MEDIA_TREE_MEDIA_BACKEND_DEBUG`` 51 | Specifies whether exceptions caused by media backends, such as ``ThumbnailError``, should be 52 | raised or silently ignored. 53 | 54 | Default: ``settings.DEBUG`` 55 | 56 | 57 | ``MEDIA_TREE_LIST_DISPLAY`` 58 | A tuple containing the columns that should be displayed in the 59 | ``FileNodeAdmin``. Note that the ``browse_controls`` column is necessary for 60 | the admin to function properly. 61 | 62 | 63 | ``MEDIA_TREE_LIST_FILTER`` 64 | A tuple containing the fields that nodes can be filtered by in the 65 | ``FileNodeAdmin``. 66 | 67 | 68 | ``MEDIA_TREE_SEARCH_FIELDS`` 69 | A tuple containing the fields that nodes can be searched by in the 70 | ``FileNodeAdmin``. 71 | 72 | 73 | ``MEDIA_TREE_UPLOAD_SUBDIR`` 74 | Default: ``'upload'`` 75 | 76 | The name of the folder under your ``MEDIA_ROOT`` where media files are stored. 77 | 78 | 79 | ``MEDIA_TREE_PREVIEW_SUBDIR`` 80 | Default: ``'upload/_preview'`` 81 | 82 | The name of the folder under your ``MEDIA_ROOT`` where cached versions of 83 | media files, e.g. thumbnails, are stored. 84 | 85 | 86 | ``MEDIA_TREE_ICON_DIRS`` 87 | Default:: 88 | 89 | ( 90 | 'media_tree/img/icons/mimetypes', 91 | ) 92 | 93 | A tuple containing all icon directories. See :ref:`install-icon-sets` 94 | for more information. 95 | 96 | 97 | ``MEDIA_TREE_THUMBNAIL_SIZES`` 98 | A dictionary of default thumbnail sizes. You can pass the dictionary key to 99 | the ``thumbnail`` templatetag instead of a numeric size. 100 | 101 | Default:: 102 | 103 | { 104 | 'small': (80, 80), 105 | 'default': (100, 100), 106 | 'medium': (250, 250), 107 | 'large': (400, 400), 108 | 'full': None, # None means: use original size 109 | } 110 | 111 | 112 | ``MEDIA_TREE_ALLOWED_FILE_TYPES`` 113 | A whitelist of file extensions that can be uploaded. By default, this is a 114 | comprehensive list of many common media file extensions that generally 115 | shouldn't pose a security risk. 116 | 117 | .. Warning:: 118 | Just because a file extension may be considered “safe”, there is 119 | absolutely no guarantee that a skilled attacker couldn't find an exploit. 120 | You should only allow people you trust to upload files to your webserver. 121 | Be careful when adding potentially unsafe file extensions to this 122 | setting, such as executables or scripts, as this possibly opens a door to 123 | attackers. 124 | 125 | 126 | ``MEDIA_TREE_THUMBNAIL_EXTENSIONS`` 127 | Default: ``('jpg', 'png')`` 128 | 129 | A tuple of image extensions used for thumbnail files. Note that ``png`` is 130 | in there since you typically might want to preserve the file type of PNG 131 | images instead of converting them to JPG. 132 | 133 | 134 | ``MEDIA_TREE_FILE_SIZE_LIMIT`` 135 | Default: ``1000000000 # 1 GB`` 136 | 137 | Maximum file size for uploaded files. 138 | 139 | 140 | ``MEDIA_TREE_GLOBAL_THUMBNAIL_OPTIONS`` 141 | A dictionary of options that should be applied by default when generating 142 | thumbnails. You might use this, for instance, to sharpen all thumbnails:: 143 | 144 | MEDIA_TREE_GLOBAL_THUMBNAIL_OPTIONS = { 145 | 'sharpen': True 146 | } 147 | -------------------------------------------------------------------------------- /media_tree/utils/staticfiles.py: -------------------------------------------------------------------------------- 1 | from media_tree import settings as app_settings 2 | from media_tree.utils import get_module_attr 3 | from django.conf import settings 4 | from django.db.models.fields.files import FieldFile 5 | from django.core.files.storage import FileSystemStorage 6 | from django.core.files.base import File 7 | from django.utils.translation import ugettext_lazy as _ 8 | from PIL import Image 9 | import os 10 | 11 | 12 | ICON_DIRS = app_settings.MEDIA_TREE_ICON_DIRS 13 | 14 | 15 | def get_static_storage(): 16 | # Passing FileSystemStorage the STATIC_ROOT and STATIC_URL settings 17 | # if set will make it work with django.contrib.staticfiles while, if 18 | # those settings don't exist, it will fall back to MEDIA_ROOT / 19 | # MEDIA_URL by default. 20 | return FileSystemStorage( 21 | location=getattr(settings, 'STATIC_ROOT', None), 22 | base_url=getattr(settings, 'STATIC_URL', None)) 23 | 24 | 25 | STATIC_STORAGE = get_static_storage() 26 | BUFFERED_ICON_SIZES = {} 27 | EXISTING_PATHS = {} 28 | 29 | 30 | class StaticFile(FieldFile): 31 | """ 32 | A wrapper for static files that is compatible to the FieldFile class, i.e. 33 | you can use instances of this class in templates just like you use the value 34 | of FileFields (e.g. `{{ my_static_file.url }}`) 35 | """ 36 | def __init__(self, instance, name): 37 | File.__init__(self, None, name) 38 | self.instance = instance 39 | self.storage = STATIC_STORAGE 40 | self.field = self 41 | 42 | def save(self, *args, **kwargs): 43 | raise NotImplementedError('Static files are read-only') 44 | 45 | def delete(self, *args, **kwargs): 46 | raise NotImplementedError('Static files are read-only') 47 | 48 | def __unicode__(self): 49 | return self.instance.__unicode__() 50 | 51 | def alt(self): 52 | return self.instance.alt 53 | 54 | 55 | class StaticIconFile(StaticFile): 56 | """ 57 | A wrapper for static icons that is compatible to the FieldFile class, i.e. 58 | you can use instances of this class in templates just like you use the value 59 | of FileFields (e.g. `{{ my_static_file.url }}`) 60 | """ 61 | def __init__(self, *args, **kwargs): 62 | super(StaticIconFile, self).__init__(*args, **kwargs) 63 | self.is_icon = True 64 | if not self.path in BUFFERED_ICON_SIZES: 65 | try: 66 | im = Image.open(self.path) 67 | BUFFERED_ICON_SIZES[self.path] = im.size 68 | except: 69 | pass 70 | self.width, self.height = BUFFERED_ICON_SIZES.get(self.path, (None, None)) 71 | 72 | def alt(self): 73 | if not self.instance: 74 | return '' 75 | if not self.instance.is_folder(): 76 | return self.instance.extension 77 | return _('folder') 78 | 79 | 80 | class StaticPathFinder: 81 | 82 | @staticmethod 83 | def find(names, dirs, file_ext): 84 | """ 85 | Iterating a set of dirs under the static root, this method tries to find 86 | a file named like one of the names and file ext passed, and returns the 87 | storage path to the first file it encounters. 88 | 89 | Usage this method makes it possible to override static files (such as 90 | icon sets) in a similar way like templates in different locations can 91 | override others that have the same file name. 92 | """ 93 | if not isinstance(names, list) or isinstance(names, tuple): 94 | names = (names,) 95 | for dir_name in dirs: 96 | for name in names: 97 | path = os.path.join(dir_name, name + file_ext) 98 | if not path in EXISTING_PATHS: 99 | # check on file system, then cache 100 | EXISTING_PATHS[path] = STATIC_STORAGE.exists(path) 101 | if EXISTING_PATHS[path]: 102 | return path 103 | 104 | 105 | class MimetypeStaticIconFileFinder: 106 | 107 | @staticmethod 108 | def find(file_node, dirs=ICON_DIRS, default_name=None, file_ext='.png'): 109 | """ 110 | Iterating all icon dirs, try to find a file called like the node's 111 | extension / mime subtype / mime type (in that order). 112 | For instance, for an MP3 file ("audio/mpeg"), this would look for: 113 | "mp3.png" / "audio/mpeg.png" / "audio.png" 114 | """ 115 | names = [] 116 | for attr_name in ('extension', 'mimetype', 'mime_supertype'): 117 | attr = getattr(file_node, attr_name) 118 | if attr: 119 | names.append(attr) 120 | if default_name: 121 | names.append(default_name) 122 | icon_path = StaticPathFinder.find(names, dirs, file_ext) 123 | if icon_path: 124 | return StaticIconFile(file_node, icon_path) 125 | 126 | 127 | # TODO: Find a better way of caching ICON_FINDERS (and what about threads and different settings files defining different icon finders?) 128 | ICON_FINDERS = None 129 | 130 | def get_icon_finders(finder_names): 131 | global ICON_FINDERS 132 | if not ICON_FINDERS: 133 | finders = [] 134 | for finder_name in finder_names: 135 | finder = get_module_attr(finder_name) 136 | if not hasattr(finder, 'find'): 137 | raise ImproperlyConfigured('Module "%s" does not define a "find" method' % finder_name) 138 | finders.append(finder) 139 | ICON_FINDERS = finders 140 | return ICON_FINDERS 141 | -------------------------------------------------------------------------------- /docs/dummy/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for dummy project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@example.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': '', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute filesystem path to the directory that will hold user-uploaded files. 47 | # Example: "/home/media/media.lawrence.com/media/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash. 52 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # Absolute path to the directory static files should be collected to. 56 | # Don't put anything in this directory yourself; store your static files 57 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 58 | # Example: "/home/media/media.lawrence.com/static/" 59 | STATIC_ROOT = '' 60 | 61 | # URL prefix for static files. 62 | # Example: "http://media.lawrence.com/static/" 63 | STATIC_URL = '/static/' 64 | 65 | # URL prefix for admin static files -- CSS, JavaScript and images. 66 | # Make sure to use a trailing slash. 67 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 68 | ADMIN_MEDIA_PREFIX = '/static/admin/' 69 | 70 | # Additional locations of static files 71 | STATICFILES_DIRS = ( 72 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 73 | # Always use forward slashes, even on Windows. 74 | # Don't forget to use absolute paths, not relative paths. 75 | ) 76 | 77 | # List of finder classes that know how to find static files in 78 | # various locations. 79 | STATICFILES_FINDERS = ( 80 | 'django.contrib.staticfiles.finders.FileSystemFinder', 81 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 82 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 83 | ) 84 | 85 | # Make this unique, and don't share it with anybody. 86 | SECRET_KEY = 'f!ctm1x10dqn1jily1rueh(uu_qv5q#nia@x034k*_*+k+!b83' 87 | 88 | # List of callables that know how to import templates from various sources. 89 | TEMPLATE_LOADERS = ( 90 | 'django.template.loaders.filesystem.Loader', 91 | 'django.template.loaders.app_directories.Loader', 92 | # 'django.template.loaders.eggs.Loader', 93 | ) 94 | 95 | MIDDLEWARE_CLASSES = ( 96 | 'django.middleware.common.CommonMiddleware', 97 | 'django.contrib.sessions.middleware.SessionMiddleware', 98 | 'django.middleware.csrf.CsrfViewMiddleware', 99 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 100 | 'django.contrib.messages.middleware.MessageMiddleware', 101 | ) 102 | 103 | ROOT_URLCONF = 'dummy.urls' 104 | 105 | TEMPLATE_DIRS = ( 106 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 107 | # Always use forward slashes, even on Windows. 108 | # Don't forget to use absolute paths, not relative paths. 109 | ) 110 | 111 | INSTALLED_APPS = ( 112 | 'django.contrib.auth', 113 | 'django.contrib.contenttypes', 114 | 'django.contrib.sessions', 115 | 'django.contrib.sites', 116 | 'django.contrib.messages', 117 | 'django.contrib.staticfiles', 118 | # Uncomment the next line to enable the admin: 119 | # 'django.contrib.admin', 120 | # Uncomment the next line to enable admin documentation: 121 | # 'django.contrib.admindocs', 122 | ) 123 | 124 | # A sample logging configuration. The only tangible logging 125 | # performed by this configuration is to send an email to 126 | # the site admins on every HTTP 500 error. 127 | # See http://docs.djangoproject.com/en/dev/topics/logging for 128 | # more details on how to customize your logging configuration. 129 | LOGGING = { 130 | 'version': 1, 131 | 'disable_existing_loggers': False, 132 | 'handlers': { 133 | 'mail_admins': { 134 | 'level': 'ERROR', 135 | 'class': 'django.utils.log.AdminEmailHandler' 136 | } 137 | }, 138 | 'loggers': { 139 | 'django.request': { 140 | 'handlers': ['mail_admins'], 141 | 'level': 'ERROR', 142 | 'propagate': True, 143 | }, 144 | } 145 | } 146 | 147 | MEDIA_TREE_MEDIA_BACKENDS = ( 148 | 'dummy.media_backends.DummyBackend', 149 | ) 150 | --------------------------------------------------------------------------------