Add FileColumn

This commit is contained in:
Bradley Ayers 2012-09-25 21:48:24 +10:00
parent cd00495839
commit 9570fed564
19 changed files with 260 additions and 40 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
/build/
/docs/_build/
/example/database.sqlite
/example/.env

View File

@ -63,6 +63,11 @@ globally, use::
Change log
==========
v0.13.0
-------
- Add FileColumn.
v0.12.1
-------

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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:
----------------------------

View File

@ -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"
}
}
]
]

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

2
example/requirements.pip Normal file
View File

@ -0,0 +1,2 @@
-e ..
django-debug-toolbar

View File

@ -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

View File

@ -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,
}),
)

View File

@ -0,0 +1 @@
bar

View File

@ -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])