Add FileColumn
This commit is contained in:
parent
cd00495839
commit
9570fed564
|
@ -13,3 +13,4 @@
|
|||
/build/
|
||||
/docs/_build/
|
||||
/example/database.sqlite
|
||||
/example/.env
|
||||
|
|
|
@ -63,6 +63,11 @@ globally, use::
|
|||
Change log
|
||||
==========
|
||||
|
||||
v0.13.0
|
||||
-------
|
||||
|
||||
- Add FileColumn.
|
||||
|
||||
v0.12.1
|
||||
-------
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
# pylint: disable=W0611
|
||||
from .tables import Table
|
||||
from .columns import (BooleanColumn, Column, CheckBoxColumn, DateColumn,
|
||||
DateTimeColumn, LinkColumn, TemplateColumn, EmailColumn,
|
||||
URLColumn)
|
||||
DateTimeColumn, EmailColumn, FileColumn, LinkColumn,
|
||||
TemplateColumn, URLColumn)
|
||||
from .config import RequestConfig
|
||||
from .utils import A, Attrs
|
||||
try:
|
||||
|
|
|
@ -4,6 +4,7 @@ from .checkboxcolumn import CheckBoxColumn
|
|||
from .datecolumn import DateColumn
|
||||
from .datetimecolumn import DateTimeColumn
|
||||
from .emailcolumn import EmailColumn
|
||||
from .filecolumn import FileColumn
|
||||
from .linkcolumn import LinkColumn
|
||||
from .templatecolumn import TemplateColumn
|
||||
from .urlcolumn import URLColumn
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# coding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.utils import A, AttributeDict
|
||||
import os
|
||||
import warnings
|
||||
from .base import Column, library
|
||||
|
||||
|
||||
@library.register
|
||||
class FileColumn(Column):
|
||||
"""
|
||||
Renders a FieldFile (or other storage backend File) as a link.
|
||||
|
||||
In addition to ``attrs`` keys supported by ``Column``, the following are
|
||||
available:
|
||||
|
||||
:type verify_exists: bool
|
||||
:param verify_exists: *try* to determine if the file actually exists.
|
||||
|
||||
- *a* -- ``<a>`` elements in ``<td>``
|
||||
- *span* -- ``<span>`` elements in ``<td>`` (missing files)
|
||||
|
||||
if *verify_exists*, the HTML class ``exists`` or ``missing`` is added to
|
||||
the element.
|
||||
"""
|
||||
def __init__(self, verify_exists=True, **kwargs):
|
||||
self.verify_exists = True
|
||||
super(FileColumn, self).__init__(**kwargs)
|
||||
|
||||
def render(self, value):
|
||||
storage = getattr(value, "storage", None)
|
||||
exists = None
|
||||
url = None
|
||||
if storage:
|
||||
# we'll assume value is a `django.db.models.fields.files.FieldFile`
|
||||
fieldfile = value
|
||||
if self.verify_exists:
|
||||
exists = storage.exists(value.name)
|
||||
url = storage.url(value.name)
|
||||
|
||||
else:
|
||||
if self.verify_exists and hasattr(value, "name"):
|
||||
# ignore negatives, perhaps the file has a name but it doesn't
|
||||
# represent a local path... better to stay neutral than give a
|
||||
# false negative.
|
||||
exists = os.path.exists(value.name) or exists
|
||||
|
||||
tag = 'a' if url else 'span'
|
||||
attrs = AttributeDict(self.attrs.get(tag, {}))
|
||||
attrs['title'] = value.name
|
||||
if url:
|
||||
attrs['href'] = url
|
||||
|
||||
# add "exists" or "missing" to the class list
|
||||
classes = [c for c in attrs.get('class', '').split(' ') if c]
|
||||
if exists is True:
|
||||
classes.append("exists")
|
||||
elif exists is False:
|
||||
classes.append("missing")
|
||||
attrs['class'] = " ".join(classes)
|
||||
|
||||
html ='<{tag} {attrs}>{text}</{tag}>'.format(
|
||||
tag=tag,
|
||||
attrs=attrs.as_html(),
|
||||
text=os.path.basename(value.name))
|
||||
return mark_safe(html)
|
||||
|
||||
@classmethod
|
||||
def from_field(cls, field):
|
||||
if isinstance(field, models.FileField):
|
||||
return cls(verbose_name=field.verbose_name)
|
|
@ -36,9 +36,9 @@ class BaseLinkColumn(Column):
|
|||
"""
|
||||
attrs = AttributeDict(attrs if attrs is not None else
|
||||
self.attrs.get('a', {}))
|
||||
html = '<a href="{uri}"{attrs}>{text}</a>'.format(
|
||||
uri=escape(uri),
|
||||
attrs=" %s" % attrs.as_html() if attrs else "",
|
||||
attrs['href'] = uri
|
||||
html = '<a {attrs}>{text}</a>'.format(
|
||||
attrs=attrs.as_html(),
|
||||
text=escape(text)
|
||||
)
|
||||
return mark_safe(html)
|
||||
|
|
|
@ -90,6 +90,7 @@ table.paleblue + ul.pagination > li:first-child {
|
|||
|
||||
table.paleblue + ul.pagination > li.cardinality {
|
||||
float: right;
|
||||
color: #8d8d8d;
|
||||
}
|
||||
|
||||
table.paleblue > tbody > tr > td > span.true,
|
||||
|
@ -103,6 +104,16 @@ table.paleblue > tbody > tr > td > span.false {
|
|||
width: 10px;
|
||||
}
|
||||
|
||||
table.paleblue > tbody > tr > td > .missing {
|
||||
background: transparent url(../img/missing.png) right center no-repeat;
|
||||
color: #717171;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
table.paleblue > tbody > tr > td > .missing:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
table.paleblue > tbody > tr > td > span.true {
|
||||
background-image: url(../img/true.gif);
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 509 B |
|
@ -498,6 +498,7 @@ For common use-cases the following columns are included:
|
|||
- :class:`.CheckBoxColumn` -- renders checkbox form inputs
|
||||
- :class:`.DateColumn` -- date formatting
|
||||
- :class:`.DateTimeColumn` -- datetime formatting in the local timezone
|
||||
- :class:`.FileColumn` -- renders files as links
|
||||
- :class:`.EmailColumn` -- renders ``<a href="mailto:...">`` tags
|
||||
- :class:`.LinkColumn` -- renders ``<a href="...">`` tags (absolute url)
|
||||
- :class:`.TemplateColumn` -- renders template code
|
||||
|
@ -1086,6 +1087,13 @@ API Reference
|
|||
:members:
|
||||
|
||||
|
||||
:class:`FileColumn` Objects:
|
||||
----------------------------
|
||||
|
||||
.. autoclass:: django_tables2.columns.FileColumn
|
||||
:members:
|
||||
|
||||
|
||||
:class:`LinkColumn` Objects:
|
||||
----------------------------
|
||||
|
||||
|
|
|
@ -1,42 +1,46 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "app.country",
|
||||
"pk": 1,
|
||||
"model": "app.country",
|
||||
"fields": {
|
||||
"tz": "Australia/Brisbane",
|
||||
"name": "Australia",
|
||||
"visits": 2,
|
||||
"population": 20000000
|
||||
"tz": "Australia/Brisbane",
|
||||
"name": "Australia",
|
||||
"visits": 2,
|
||||
"population": 20000000,
|
||||
"flag": "country/flags/australia.svg"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "app.country",
|
||||
"pk": 2,
|
||||
"model": "app.country",
|
||||
"fields": {
|
||||
"tz": "NZST",
|
||||
"name": "New Zealand",
|
||||
"visits": 1,
|
||||
"population": 12000000
|
||||
"tz": "NZST",
|
||||
"name": "New Zealand",
|
||||
"visits": 1,
|
||||
"population": 12000000,
|
||||
"flag": "country/flags/new_zealand.svg"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "app.country",
|
||||
"pk": 3,
|
||||
"model": "app.country",
|
||||
"fields": {
|
||||
"tz": "CAT",
|
||||
"name": "Africa",
|
||||
"visits": 0,
|
||||
"population": 1000010000
|
||||
"tz": "CAT",
|
||||
"name": "Africa",
|
||||
"visits": 0,
|
||||
"population": 1000010000,
|
||||
"flag": "country/flags/africa.svg"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"pk": 4,
|
||||
"model": "app.country",
|
||||
"pk": 4,
|
||||
"model": "app.country",
|
||||
"fields": {
|
||||
"tz": "UTC\u22123.5",
|
||||
"name": "Canada",
|
||||
"visits": 1,
|
||||
"population": 34447000
|
||||
"tz": "UTC\u22123.5",
|
||||
"name": "Canada",
|
||||
"visits": 1,
|
||||
"population": 34447000,
|
||||
"flag": "country/flags/canada.svg"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -11,6 +11,7 @@ class Country(models.Model):
|
|||
tz = models.CharField(max_length=50)
|
||||
visits = models.PositiveIntegerField()
|
||||
commonwealth = models.NullBooleanField()
|
||||
flag = models.FileField(upload_to="country/flags/")
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("countries")
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="640" viewBox="0 0 30240 15120"><defs id="defs6"><polygon points="0,-9 1.735535,-3.6038755 7.0364833,-5.6114082 3.8997116,-0.89008374 8.7743512,2.0026884 3.1273259,2.4939592 3.9049537,8.1087198 0,4 -3.9049537,8.1087198 -3.1273259,2.4939592 -8.7743512,2.0026884 -3.8997116,-0.89008374 -7.0364833,-5.6114082 -1.735535,-3.6038755 0,-9 " id="Star7"/><polygon points="0,-9 2.351141,-3.236068 8.5595086,-2.7811529 3.8042261,1.236068 5.2900673,7.2811529 0,4 -5.2900673,7.2811529 -3.8042261,1.236068 -8.5595086,-2.7811529 -2.351141,-3.236068 0,-9 " id="Star5"/></defs><path d="M 0,0 L 30240,0 L 30240,15120 L 0,15120 L 0,0 z" style="fill:#00008b"/><use transform="matrix(252,0,0,252,7560,11340)" id="Commonwealth_Star" style="fill:#fff" xlink:href="#Star7"/><use transform="matrix(120,0,0,120,22680,12600)" id="Star_Alpha_Crucis" style="fill:#fff" xlink:href="#Star7"/><use transform="matrix(120,0,0,120,18900,6615)" id="Star_Beta_Crucis" style="fill:#fff" xlink:href="#Star7"/><use transform="matrix(120,0,0,120,22680,2520)" id="Star_Gamma_Crucis" style="fill:#fff" xlink:href="#Star7"/><use transform="matrix(120,0,0,120,26040,5607)" id="Star_Delta_Crucis" style="fill:#fff" xlink:href="#Star7"/><use transform="matrix(70,0,0,70,24192,8190)" id="Star_Epsilon_Crucis" style="fill:#fff" xlink:href="#Star5"/><path d="M 6300,0 L 8820,0 L 8820,2520 L 15120,2520 L 15120,5040 L 8820,5040 L 8820,7560 L 6300,7560 L 6300,5040 L 0,5040 L 0,2520 L 6300,2520 L 6300,0 z" id="White_Cross" style="fill:#fff"/><path d="M 0,0 L 1690.4674,0 L 15120,6714.7663 L 15120,7560 L 13429.533,7560 L 0,845.2337 L 0,0 z" id="White_Diagonal" style="fill:#fff"/><use transform="matrix(-1,0,0,1,15120,0)" id="White_Diagonal_Flipped" style="fill:#fff" xlink:href="#White_Diagonal"/><path d="M 6804,0 L 8316,0 L 8316,3024 L 15120,3024 L 15120,4536 L 8316,4536 L 8316,7560 L 6804,7560 L 6804,4536 L 0,4536 L 0,3024 L 6804,3024 L 6804,0 z" id="Red_Cross" style="fill:red"/><path d="M 0,0 L 5040,2520 L 3913.0217,2520 L 0,563.48913 L 0,0 z M 15120,0 L 13993.022,0 L 8953.0217,2520 L 10080,2520 L 15120,0 z" id="Red_Diagonals" style="fill:red"/><use transform="matrix(-1,0,0,-1,15120,7560)" id="Red_Diagonals_Rotated" style="fill:red" xlink:href="#Red_Diagonals"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1000" height="500"><rect width="1000" height="500" fill="#f00"/><rect width="500" height="500" fill="#fff" x="250"/><path fill="#ff0000" d="m 499.99228,46.875 -34.11312,63.62529 c -3.87086,6.91501 -10.80627,6.27363 -17.74169,2.41138 l -24.69699,-12.78858 18.40705,97.72711 c 3.87086,17.85419 -8.54859,17.85419 -14.67765,10.13435 l -43.10105,-48.25099 -6.99738,24.503 c -0.80692,3.21777 -4.35481,6.59744 -9.67748,5.79261 l -54.50177,-11.45912 14.31524,52.04475 c 3.06451,11.58054 5.4549,16.37528 -3.09375,19.42959 l -19.42619,9.13025 93.82127,76.20838 c 3.7135,2.88151 5.58971,8.067 4.26768,12.7621 l -8.21136,26.94707 c 32.30405,-3.72371 61.24898,-9.32594 93.56939,-12.77619 2.85323,-0.30459 7.62988,4.40408 7.61029,7.71058 l -4.28024,98.72342 15.70639,0 -2.47237,-98.5117 c -0.0197,-3.3065 4.31372,-8.22689 7.16695,-7.9223 32.32041,3.45026 61.26538,9.05248 93.56942,12.77619 l -8.21134,-26.94707 c -1.32203,-4.6951 0.55417,-9.88059 4.26767,-12.7621 l 93.82125,-76.20838 -19.42617,-9.13025 c -8.54867,-3.05431 -6.15828,-7.84905 -3.09377,-19.42959 l 14.31527,-52.04475 -54.5018,11.45912 c -5.32267,0.80483 -8.87056,-2.57484 -9.6775,-5.79261 l -6.99737,-24.503 -43.10103,48.25099 c -6.12908,7.71984 -18.54854,7.71984 -14.67768,-10.13435 l 18.40702,-97.72711 -24.69694,12.78858 c -6.93559,3.86225 -13.87083,4.50363 -17.7417,-2.41138"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" xmlns:xlink="http://www.w3.org/1999/xlink" width="960" height="480" viewBox="0 0 240 120">
|
||||
<title>Flag of New Zealand</title>
|
||||
|
||||
<rect fill="#00247d" width="240" height="120"/>
|
||||
<svg width="120" height="60" viewBox="0 0 12 6">
|
||||
<clipPath id="t">
|
||||
<path d="M6,3 h6 v3 z v3 h-6 z h-6 v-3 z v-3 h6 z"/>
|
||||
</clipPath>
|
||||
<g stroke="#cf142b" stroke-width="1.2">
|
||||
<path d="M0,0 L12,6 M12,0 L0,6" stroke="#fff"/>
|
||||
<path d="M0,0 L12,6 M12,0 L0,6" clip-path="url(#t)" stroke-width="0.8"/>
|
||||
<path d="M6,0 v6 M0,3 h12" stroke="#fff" stroke-width="2"/>
|
||||
<path d="M6,0 v6 M0,3 h12"/>
|
||||
</g>
|
||||
</svg>
|
||||
<polygon id="star" points="0,-513674 301930,415571 -488533,-158734 488533,-158734 -301930,415571" transform="scale(0.00000102347231405)"/>
|
||||
<g transform="translate(180,24)">
|
||||
<use xlink:href="#star" fill="#fff" transform="scale(18.472135955)"/>
|
||||
<use xlink:href="#star" fill="#cc142b" transform="scale(12)"/>
|
||||
</g>
|
||||
<g transform="translate(180,96)">
|
||||
<use xlink:href="#star" fill="#fff" transform="scale(20.472135955)"/>
|
||||
<use xlink:href="#star" fill="#cc142b" transform="scale(14)"/>
|
||||
</g>
|
||||
<g transform="rotate(-8,180,48) translate(204,48) rotate(8)">
|
||||
<use xlink:href="#star" fill="#fff" transform="scale(16.472135955)"/>
|
||||
<use xlink:href="#star" fill="#cc142b" transform="scale(10)"/>
|
||||
</g>
|
||||
<g transform="rotate(-8,180,48) translate(152,48) rotate(8)">
|
||||
<use xlink:href="#star" fill="#fff" transform="scale(18.472135955)"/>
|
||||
<use xlink:href="#star" fill="#cc142b" transform="scale(12)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,2 @@
|
|||
-e ..
|
||||
django-debug-toolbar
|
|
@ -50,12 +50,12 @@ USE_L10N = True
|
|||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
MEDIA_ROOT = join(ROOT, 'media')
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# coding: utf-8
|
||||
from django.conf.urls.defaults import patterns, include, url
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -11,9 +12,10 @@ urlpatterns = patterns('example.app.views',
|
|||
url(r'^class-based/$', 'class_based'),
|
||||
url(r'^tutorial/$', 'tutorial'),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
) + patterns('',
|
||||
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
|
||||
'document_root': settings.MEDIA_ROOT,
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
bar
|
|
@ -8,7 +8,10 @@ import django_tables2 as tables
|
|||
from django_tables2.utils import build_request
|
||||
from django_tables2 import A, Attrs
|
||||
from django.db import models
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.template import Context, Template
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.safestring import mark_safe, SafeData
|
||||
|
@ -16,6 +19,7 @@ try:
|
|||
from django.utils import timezone
|
||||
except ImportError:
|
||||
timezone = None
|
||||
from os.path import dirname, join
|
||||
import pytz
|
||||
from .app.models import Person
|
||||
from .templates import attrs, parse
|
||||
|
@ -742,5 +746,73 @@ def should_be_used_for_datetimefields():
|
|||
assert type(Table.base_columns["field"]) == tables.DateTimeColumn
|
||||
|
||||
|
||||
filecolumn = Tests()
|
||||
|
||||
|
||||
@filecolumn.context
|
||||
def template_storage_and_column():
|
||||
"""Provide a storage that exposes the test templates"""
|
||||
root = join(dirname(__file__), "app", "templates")
|
||||
storage = FileSystemStorage(location=root, base_url="/baseurl/")
|
||||
column = tables.FileColumn(attrs={"span": {"class": "span"},
|
||||
"a": {"class": "a"}})
|
||||
yield column, storage
|
||||
|
||||
|
||||
@filecolumn.test
|
||||
def should_be_used_for_filefields():
|
||||
class FileModel(models.Model):
|
||||
field = models.FileField()
|
||||
|
||||
class Table(tables.Table):
|
||||
class Meta:
|
||||
model = FileModel
|
||||
|
||||
assert type(Table.base_columns["field"]) == tables.FileColumn
|
||||
|
||||
|
||||
@filecolumn.test
|
||||
def filecolumn_supports_storage_file(column, storage):
|
||||
with storage.open("child/foo.html") as file_:
|
||||
root = parse(column.render(value=file_))
|
||||
path = file_.name
|
||||
assert root.tag == "span"
|
||||
assert root.attrib == {"class": "span exists", "title": path}
|
||||
assert root.text == "foo.html"
|
||||
|
||||
|
||||
@filecolumn.test
|
||||
def filecolumn_supports_contentfile(column):
|
||||
name = "foobar.html"
|
||||
file_ = ContentFile('', name=name)
|
||||
root = parse(column.render(value=file_))
|
||||
assert root.tag == "span"
|
||||
assert root.attrib == {"title": name, "class": "span"}
|
||||
assert root.text == "foobar.html"
|
||||
|
||||
|
||||
@filecolumn.test
|
||||
def filecolumn_supports_fieldfile(column, storage):
|
||||
field = models.FileField(storage=storage)
|
||||
name = "child/foo.html"
|
||||
fieldfile = FieldFile(instance=None, field=field, name=name)
|
||||
root = parse(column.render(value=fieldfile))
|
||||
assert root.tag == "a"
|
||||
assert root.attrib == {"class": "a exists", "title": name,
|
||||
"href": "/baseurl/child/foo.html"}
|
||||
assert root.text == "foo.html"
|
||||
|
||||
# Now try a file that doesn't exist
|
||||
name = "child/does_not_exist.html"
|
||||
fieldfile = FieldFile(instance=None, field=field, name=name)
|
||||
html = column.render(value=fieldfile)
|
||||
root = parse(html)
|
||||
assert root.tag == "a"
|
||||
assert root.attrib == {"class": "a missing", "title": name,
|
||||
"href": "/baseurl/child/does_not_exist.html"}
|
||||
assert root.text == "does_not_exist.html"
|
||||
|
||||
|
||||
columns = Tests([booleancolumn, checkboxcolumn, datecolumn, datetimecolumn,
|
||||
emailcolumn, general, linkcolumn, templatecolumn, urlcolumn])
|
||||
emailcolumn, filecolumn, general, linkcolumn, templatecolumn,
|
||||
urlcolumn])
|
||||
|
|
Loading…
Reference in New Issue