aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules4
-rw-r--r--AUTHORS3
-rw-r--r--docs/source/siteadmin/deploying.rst26
-rwxr-xr-xexperimental-bootstrap.sh (renamed from bootstrap.sh)0
-rwxr-xr-xextlib/exif/EXIF.py1896
-rw-r--r--extlib/exif/LICENSE1
-rw-r--r--extlib/exif/changes.txt131
m---------extlib/skeleton0
-rw-r--r--mediagoblin.ini4
-rw-r--r--mediagoblin/auth/tools.py2
-rw-r--r--mediagoblin/config_spec.ini2
-rw-r--r--mediagoblin/db/mixin.py6
-rw-r--r--mediagoblin/gmg_commands/__init__.py4
-rw-r--r--mediagoblin/gmg_commands/deletemedia.py38
-rw-r--r--mediagoblin/media_types/blog/lib.py9
-rw-r--r--mediagoblin/media_types/blog/models.py6
-rw-r--r--mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_post_listing.html2
-rw-r--r--mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html4
-rw-r--r--mediagoblin/media_types/blog/views.py23
-rw-r--r--mediagoblin/plugins/trim_whitespace/README.rst15
-rw-r--r--mediagoblin/static/css/base.css295
l---------mediagoblin/static/css/extlib/skeleton.css1
l---------mediagoblin/static/extlib/skeleton1
-rw-r--r--mediagoblin/templates/mediagoblin/banned.html2
-rw-r--r--mediagoblin/templates/mediagoblin/base.html200
-rw-r--r--mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html49
-rw-r--r--mediagoblin/templates/mediagoblin/edit/attachments.html2
-rw-r--r--mediagoblin/templates/mediagoblin/edit/delete_account.html2
-rw-r--r--mediagoblin/templates/mediagoblin/error.html2
-rw-r--r--mediagoblin/templates/mediagoblin/media_displays/stl.html5
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/report.html4
-rw-r--r--mediagoblin/templates/mediagoblin/moderation/user.html236
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html12
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html10
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html30
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html9
-rw-r--r--mediagoblin/templates/mediagoblin/utils/collection_gallery.html12
-rw-r--r--mediagoblin/templates/mediagoblin/utils/object_gallery.html12
-rw-r--r--mediagoblin/tests/__init__.py41
-rw-r--r--mediagoblin/tests/pytest.ini1
-rw-r--r--mediagoblin/tests/test_exif.py2
-rw-r--r--mediagoblin/tests/test_modelmethods.py24
-rw-r--r--mediagoblin/tests/test_submission/COPYING-psycho.txt24
-rw-r--r--mediagoblin/tests/test_submission/COPYING.txt64
-rw-r--r--mediagoblin/tests/test_submission/big-fear_of_flight_source.xcfbin0 -> 9543047 bytes
-rw-r--r--mediagoblin/tests/test_submission/big-psycho_source.blendbin0 -> 553880 bytes
-rw-r--r--mediagoblin/tests/test_submission/cc-by-sa-3.0.txt359
-rw-r--r--mediagoblin/tests/test_submission/good-original_gavroche_sketch_source.xcfbin0 -> 121512 bytes
-rw-r--r--mediagoblin/tests/test_submission/medium-robogoblins.xcfbin0 -> 20869309 bytes
-rw-r--r--mediagoblin/tests/test_util.py6
-rw-r--r--mediagoblin/themes/airy/assets/css/airy.css23
-rw-r--r--mediagoblin/tools/exif.py6
l---------mediagoblin/tools/extlib/EXIF.py1
-rw-r--r--mediagoblin/tools/translate.py2
-rw-r--r--mediagoblin/tools/url.py18
-rw-r--r--mediagoblin/user_pages/views.py20
-rw-r--r--mediagoblin/views.py13
-rw-r--r--setup.py22
59 files changed, 1203 insertions, 2485 deletions
diff --git a/.gitmodules b/.gitmodules
index 95a76e1f..20fa20e2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,4 +4,6 @@
[submodule "extlib/pdf.js"]
path = extlib/pdf.js
url = git://github.com/mozilla/pdf.js.git
-
+[submodule "extlib/skeleton"]
+ path = extlib/skeleton
+ url = git://github.com/dhg/Skeleton.git
diff --git a/AUTHORS b/AUTHORS
index 2b2bb48a..8ea903f5 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -19,6 +19,8 @@ Thank you!
* Asheesh Laroia
* Bassam Kurdali
* Bernhard Keller
+* Berker Peksag
+* Boris Bobrov
* Brandon Invergo
* Brett Smith
* Caleb Forbes Davis V
@@ -40,6 +42,7 @@ Thank you!
* Hans Lo
* Jakob Kramer
* Jef van Schendel
+* Jeremy Pope
* Jessica Tallon
* Jim Campbell
* Joar Wandborg
diff --git a/docs/source/siteadmin/deploying.rst b/docs/source/siteadmin/deploying.rst
index de4ce1ac..0dde3b6a 100644
--- a/docs/source/siteadmin/deploying.rst
+++ b/docs/source/siteadmin/deploying.rst
@@ -91,13 +91,7 @@ name will be ``mediagoblin`` too.
To create our new user, run::
- sudo -u postgres createuser mediagoblin
-
-then answer NO to *all* the questions::
-
- Shall the new role be a superuser? (y/n) n
- Shall the new role be allowed to create databases? (y/n) n
- Shall the new role be allowed to create more new roles? (y/n) n
+ sudo -u postgres createuser -A -D mediagoblin
then create the database all our MediaGoblin data should be stored in::
@@ -122,8 +116,8 @@ Drop Privileges for MediaGoblin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MediaGoblin does not require special permissions or elevated
-access to run. As such, the prefered way to run MediaGoblin is to
-create a dedicated, unpriviledged system user for sole the purpose of running
+access to run. As such, the preferred way to run MediaGoblin is to
+create a dedicated, unprivileged system user for the sole purpose of running
MediaGoblin. Running MediaGoblin processes under an unpriviledged system user
helps to keep it more secure.
@@ -136,11 +130,11 @@ username if you wish.::
No password will be assigned to this account, and you will not be able
to log in as this user. To switch to this account, enter either::
- sudo su - mediagoblin (if you have sudo permissions)
+ sudo -u mediagoblin /bin/bash # (if you have sudo permissions)
or::
- su - mediagoblin (if you have to use root permissions)
+ su mediagoblin -s /bin/bash # (if you have to use root permissions)
You may get a warning similar to this when entering these commands::
@@ -171,11 +165,11 @@ to the unpriviledged system account.
To do this, enter either of the following commands, changing the defaults
to suit your particular requirements::
- sudo mkdir -p /srv/mediagoblin.example.org && sudo chown -hR mediagoblin:mediagoblin /srv/mediagobin.example.org
+ sudo mkdir -p /srv/mediagoblin.example.org && sudo chown -hR mediagoblin:mediagoblin /srv/mediagoblin.example.org
or (as the root user)::
- mkdir -p /srv/mediagoblin.example.org && chown -hR mediagoblin:mediagoblin /srv/mediagobin.example.org
+ mkdir -p /srv/mediagoblin.example.org && chown -hR mediagoblin:mediagoblin /srv/mediagoblin.example.org
Install MediaGoblin and Virtualenv
@@ -210,14 +204,16 @@ And set up the in-package virtualenv::
.. note::
- We presently have an experimental make-style deployment system. if
+ We presently have an **experimental** make-style deployment system. if
you'd like to try it, instead of the above command, you can run::
- ./bootstrap.sh && ./configure && make
+ ./experimental-bootstrap.sh && ./configure && make
This also includes a number of nice features, such as keeping your
viratualenv up to date by simply running `make update`.
+ Note: this is liable to break. Use this method with caution.
+
.. ::
(NOTE: Is this still relevant?)
diff --git a/bootstrap.sh b/experimental-bootstrap.sh
index 78d0f1c7..78d0f1c7 100755
--- a/bootstrap.sh
+++ b/experimental-bootstrap.sh
diff --git a/extlib/exif/EXIF.py b/extlib/exif/EXIF.py
deleted file mode 100755
index a188154e..00000000
--- a/extlib/exif/EXIF.py
+++ /dev/null
@@ -1,1896 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-#
-# Library to extract EXIF information from digital camera image files.
-# https://github.com/ianare/exif-py
-#
-#
-# VERSION 1.1.0
-#
-# To use this library call with:
-# f = open(path_name, 'rb')
-# tags = EXIF.process_file(f)
-#
-# To ignore MakerNote tags, pass the -q or --quick
-# command line arguments, or as
-# tags = EXIF.process_file(f, details=False)
-#
-# To stop processing after a certain tag is retrieved,
-# pass the -t TAG or --stop-tag TAG argument, or as
-# tags = EXIF.process_file(f, stop_tag='TAG')
-#
-# where TAG is a valid tag name, ex 'DateTimeOriginal'
-#
-# These 2 are useful when you are retrieving a large list of images
-#
-# To return an error on invalid tags,
-# pass the -s or --strict argument, or as
-# tags = EXIF.process_file(f, strict=True)
-#
-# Otherwise these tags will be ignored
-#
-# Returned tags will be a dictionary mapping names of EXIF tags to their
-# values in the file named by path_name. You can process the tags
-# as you wish. In particular, you can iterate through all the tags with:
-# for tag in tags.keys():
-# if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename',
-# 'EXIF MakerNote'):
-# print "Key: %s, value %s" % (tag, tags[tag])
-# (This code uses the if statement to avoid printing out a few of the
-# tags that tend to be long or boring.)
-#
-# The tags dictionary will include keys for all of the usual EXIF
-# tags, and will also include keys for Makernotes used by some
-# cameras, for which we have a good specification.
-#
-# Note that the dictionary keys are the IFD name followed by the
-# tag name. For example:
-# 'EXIF DateTimeOriginal', 'Image Orientation', 'MakerNote FocusMode'
-#
-# Copyright (c) 2002-2007 Gene Cash All rights reserved
-# Copyright (c) 2007-2012 Ianaré Sévi All rights reserved
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials provided
-# with the distribution.
-#
-# 3. Neither the name of the authors nor the names of its contributors
-# may be used to endorse or promote products derived from this
-# software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-#
-# ----- See 'changes.txt' file for all contributors and changes ----- #
-#
-
-
-# Don't throw an exception when given an out of range character.
-def make_string(seq):
- str = ''
- for c in seq:
- # Screen out non-printing characters
- if 32 <= c and c < 256:
- str += chr(c)
- # If no printing chars
- if not str:
- return seq
- return str
-
-# Special version to deal with the code in the first 8 bytes of a user comment.
-# First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode
-def make_string_uc(seq):
- code = seq[0:8]
- seq = seq[8:]
- # Of course, this is only correct if ASCII, and the standard explicitly
- # allows JIS and Unicode.
- return make_string( make_string(seq) )
-
-# field type descriptions as (length, abbreviation, full name) tuples
-FIELD_TYPES = (
- (0, 'X', 'Proprietary'), # no such type
- (1, 'B', 'Byte'),
- (1, 'A', 'ASCII'),
- (2, 'S', 'Short'),
- (4, 'L', 'Long'),
- (8, 'R', 'Ratio'),
- (1, 'SB', 'Signed Byte'),
- (1, 'U', 'Undefined'),
- (2, 'SS', 'Signed Short'),
- (4, 'SL', 'Signed Long'),
- (8, 'SR', 'Signed Ratio'),
- )
-
-# dictionary of main EXIF tag names
-# first element of tuple is tag name, optional second element is
-# another dictionary giving names to values
-EXIF_TAGS = {
- 0x0100: ('ImageWidth', ),
- 0x0101: ('ImageLength', ),
- 0x0102: ('BitsPerSample', ),
- 0x0103: ('Compression',
- {1: 'Uncompressed',
- 2: 'CCITT 1D',
- 3: 'T4/Group 3 Fax',
- 4: 'T6/Group 4 Fax',
- 5: 'LZW',
- 6: 'JPEG (old-style)',
- 7: 'JPEG',
- 8: 'Adobe Deflate',
- 9: 'JBIG B&W',
- 10: 'JBIG Color',
- 32766: 'Next',
- 32769: 'Epson ERF Compressed',
- 32771: 'CCIRLEW',
- 32773: 'PackBits',
- 32809: 'Thunderscan',
- 32895: 'IT8CTPAD',
- 32896: 'IT8LW',
- 32897: 'IT8MP',
- 32898: 'IT8BL',
- 32908: 'PixarFilm',
- 32909: 'PixarLog',
- 32946: 'Deflate',
- 32947: 'DCS',
- 34661: 'JBIG',
- 34676: 'SGILog',
- 34677: 'SGILog24',
- 34712: 'JPEG 2000',
- 34713: 'Nikon NEF Compressed',
- 65000: 'Kodak DCR Compressed',
- 65535: 'Pentax PEF Compressed'}),
- 0x0106: ('PhotometricInterpretation', ),
- 0x0107: ('Thresholding', ),
- 0x010A: ('FillOrder', ),
- 0x010D: ('DocumentName', ),
- 0x010E: ('ImageDescription', ),
- 0x010F: ('Make', ),
- 0x0110: ('Model', ),
- 0x0111: ('StripOffsets', ),
- 0x0112: ('Orientation',
- {1: 'Horizontal (normal)',
- 2: 'Mirrored horizontal',
- 3: 'Rotated 180',
- 4: 'Mirrored vertical',
- 5: 'Mirrored horizontal then rotated 90 CCW',
- 6: 'Rotated 90 CCW',
- 7: 'Mirrored horizontal then rotated 90 CW',
- 8: 'Rotated 90 CW'}),
- 0x0115: ('SamplesPerPixel', ),
- 0x0116: ('RowsPerStrip', ),
- 0x0117: ('StripByteCounts', ),
- 0x011A: ('XResolution', ),
- 0x011B: ('YResolution', ),
- 0x011C: ('PlanarConfiguration', ),
- 0x011D: ('PageName', make_string),
- 0x0128: ('ResolutionUnit',
- {1: 'Not Absolute',
- 2: 'Pixels/Inch',
- 3: 'Pixels/Centimeter'}),
- 0x012D: ('TransferFunction', ),
- 0x0131: ('Software', ),
- 0x0132: ('DateTime', ),
- 0x013B: ('Artist', ),
- 0x013E: ('WhitePoint', ),
- 0x013F: ('PrimaryChromaticities', ),
- 0x0156: ('TransferRange', ),
- 0x0200: ('JPEGProc', ),
- 0x0201: ('JPEGInterchangeFormat', ),
- 0x0202: ('JPEGInterchangeFormatLength', ),
- 0x0211: ('YCbCrCoefficients', ),
- 0x0212: ('YCbCrSubSampling', ),
- 0x0213: ('YCbCrPositioning',
- {1: 'Centered',
- 2: 'Co-sited'}),
- 0x0214: ('ReferenceBlackWhite', ),
-
- 0x4746: ('Rating', ),
-
- 0x828D: ('CFARepeatPatternDim', ),
- 0x828E: ('CFAPattern', ),
- 0x828F: ('BatteryLevel', ),
- 0x8298: ('Copyright', ),
- 0x829A: ('ExposureTime', ),
- 0x829D: ('FNumber', ),
- 0x83BB: ('IPTC/NAA', ),
- 0x8769: ('ExifOffset', ),
- 0x8773: ('InterColorProfile', ),
- 0x8822: ('ExposureProgram',
- {0: 'Unidentified',
- 1: 'Manual',
- 2: 'Program Normal',
- 3: 'Aperture Priority',
- 4: 'Shutter Priority',
- 5: 'Program Creative',
- 6: 'Program Action',
- 7: 'Portrait Mode',
- 8: 'Landscape Mode'}),
- 0x8824: ('SpectralSensitivity', ),
- 0x8825: ('GPSInfo', ),
- 0x8827: ('ISOSpeedRatings', ),
- 0x8828: ('OECF', ),
- 0x9000: ('ExifVersion', make_string),
- 0x9003: ('DateTimeOriginal', ),
- 0x9004: ('DateTimeDigitized', ),
- 0x9101: ('ComponentsConfiguration',
- {0: '',
- 1: 'Y',
- 2: 'Cb',
- 3: 'Cr',
- 4: 'Red',
- 5: 'Green',
- 6: 'Blue'}),
- 0x9102: ('CompressedBitsPerPixel', ),
- 0x9201: ('ShutterSpeedValue', ),
- 0x9202: ('ApertureValue', ),
- 0x9203: ('BrightnessValue', ),
- 0x9204: ('ExposureBiasValue', ),
- 0x9205: ('MaxApertureValue', ),
- 0x9206: ('SubjectDistance', ),
- 0x9207: ('MeteringMode',
- {0: 'Unidentified',
- 1: 'Average',
- 2: 'CenterWeightedAverage',
- 3: 'Spot',
- 4: 'MultiSpot',
- 5: 'Pattern',
- 6: 'Partial',
- 255: 'other'}),
- 0x9208: ('LightSource',
- {0: 'Unknown',
- 1: 'Daylight',
- 2: 'Fluorescent',
- 3: 'Tungsten (incandescent light)',
- 4: 'Flash',
- 9: 'Fine weather',
- 10: 'Cloudy weather',
- 11: 'Shade',
- 12: 'Daylight fluorescent (D 5700 - 7100K)',
- 13: 'Day white fluorescent (N 4600 - 5400K)',
- 14: 'Cool white fluorescent (W 3900 - 4500K)',
- 15: 'White fluorescent (WW 3200 - 3700K)',
- 17: 'Standard light A',
- 18: 'Standard light B',
- 19: 'Standard light C',
- 20: 'D55',
- 21: 'D65',
- 22: 'D75',
- 23: 'D50',
- 24: 'ISO studio tungsten',
- 255: 'other light source',}),
- 0x9209: ('Flash',
- {0: 'Flash did not fire',
- 1: 'Flash fired',
- 5: 'Strobe return light not detected',
- 7: 'Strobe return light detected',
- 9: 'Flash fired, compulsory flash mode',
- 13: 'Flash fired, compulsory flash mode, return light not detected',
- 15: 'Flash fired, compulsory flash mode, return light detected',
- 16: 'Flash did not fire, compulsory flash mode',
- 24: 'Flash did not fire, auto mode',
- 25: 'Flash fired, auto mode',
- 29: 'Flash fired, auto mode, return light not detected',
- 31: 'Flash fired, auto mode, return light detected',
- 32: 'No flash function',
- 65: 'Flash fired, red-eye reduction mode',
- 69: 'Flash fired, red-eye reduction mode, return light not detected',
- 71: 'Flash fired, red-eye reduction mode, return light detected',
- 73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
- 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
- 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
- 89: 'Flash fired, auto mode, red-eye reduction mode',
- 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
- 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'}),
- 0x920A: ('FocalLength', ),
- 0x9214: ('SubjectArea', ),
- 0x927C: ('MakerNote', ),
- 0x9286: ('UserComment', make_string_uc),
- 0x9290: ('SubSecTime', ),
- 0x9291: ('SubSecTimeOriginal', ),
- 0x9292: ('SubSecTimeDigitized', ),
-
- # used by Windows Explorer
- 0x9C9B: ('XPTitle', ),
- 0x9C9C: ('XPComment', ),
- 0x9C9D: ('XPAuthor', ), #(ignored by Windows Explorer if Artist exists)
- 0x9C9E: ('XPKeywords', ),
- 0x9C9F: ('XPSubject', ),
-
- 0xA000: ('FlashPixVersion', make_string),
- 0xA001: ('ColorSpace',
- {1: 'sRGB',
- 2: 'Adobe RGB',
- 65535: 'Uncalibrated'}),
- 0xA002: ('ExifImageWidth', ),
- 0xA003: ('ExifImageLength', ),
- 0xA005: ('InteroperabilityOffset', ),
- 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP
- 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C
- 0xA20E: ('FocalPlaneXResolution', ), # 0x920E
- 0xA20F: ('FocalPlaneYResolution', ), # 0x920F
- 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210
- 0xA214: ('SubjectLocation', ), # 0x9214
- 0xA215: ('ExposureIndex', ), # 0x9215
- 0xA217: ('SensingMethod', # 0x9217
- {1: 'Not defined',
- 2: 'One-chip color area',
- 3: 'Two-chip color area',
- 4: 'Three-chip color area',
- 5: 'Color sequential area',
- 7: 'Trilinear',
- 8: 'Color sequential linear'}),
- 0xA300: ('FileSource',
- {1: 'Film Scanner',
- 2: 'Reflection Print Scanner',
- 3: 'Digital Camera'}),
- 0xA301: ('SceneType',
- {1: 'Directly Photographed'}),
- 0xA302: ('CVAPattern', ),
- 0xA401: ('CustomRendered',
- {0: 'Normal',
- 1: 'Custom'}),
- 0xA402: ('ExposureMode',
- {0: 'Auto Exposure',
- 1: 'Manual Exposure',
- 2: 'Auto Bracket'}),
- 0xA403: ('WhiteBalance',
- {0: 'Auto',
- 1: 'Manual'}),
- 0xA404: ('DigitalZoomRatio', ),
- 0xA405: ('FocalLengthIn35mmFilm', ),
- 0xA406: ('SceneCaptureType',
- {0: 'Standard',
- 1: 'Landscape',
- 2: 'Portrait',
- 3: 'Night)'}),
- 0xA407: ('GainControl',
- {0: 'None',
- 1: 'Low gain up',
- 2: 'High gain up',
- 3: 'Low gain down',
- 4: 'High gain down'}),
- 0xA408: ('Contrast',
- {0: 'Normal',
- 1: 'Soft',
- 2: 'Hard'}),
- 0xA409: ('Saturation',
- {0: 'Normal',
- 1: 'Soft',
- 2: 'Hard'}),
- 0xA40A: ('Sharpness',
- {0: 'Normal',
- 1: 'Soft',
- 2: 'Hard'}),
- 0xA40B: ('DeviceSettingDescription', ),
- 0xA40C: ('SubjectDistanceRange', ),
- 0xA500: ('Gamma', ),
- 0xC4A5: ('PrintIM', ),
- 0xEA1C: ('Padding', ),
- }
-
-# interoperability tags
-INTR_TAGS = {
- 0x0001: ('InteroperabilityIndex', ),
- 0x0002: ('InteroperabilityVersion', ),
- 0x1000: ('RelatedImageFileFormat', ),
- 0x1001: ('RelatedImageWidth', ),
- 0x1002: ('RelatedImageLength', ),
- }
-
-# GPS tags (not used yet, haven't seen camera with GPS)
-GPS_TAGS = {
- 0x0000: ('GPSVersionID', ),
- 0x0001: ('GPSLatitudeRef', ),
- 0x0002: ('GPSLatitude', ),
- 0x0003: ('GPSLongitudeRef', ),
- 0x0004: ('GPSLongitude', ),
- 0x0005: ('GPSAltitudeRef', ),
- 0x0006: ('GPSAltitude', ),
- 0x0007: ('GPSTimeStamp', ),
- 0x0008: ('GPSSatellites', ),
- 0x0009: ('GPSStatus', ),
- 0x000A: ('GPSMeasureMode', ),
- 0x000B: ('GPSDOP', ),
- 0x000C: ('GPSSpeedRef', ),
- 0x000D: ('GPSSpeed', ),
- 0x000E: ('GPSTrackRef', ),
- 0x000F: ('GPSTrack', ),
- 0x0010: ('GPSImgDirectionRef', ),
- 0x0011: ('GPSImgDirection', ),
- 0x0012: ('GPSMapDatum', ),
- 0x0013: ('GPSDestLatitudeRef', ),
- 0x0014: ('GPSDestLatitude', ),
- 0x0015: ('GPSDestLongitudeRef', ),
- 0x0016: ('GPSDestLongitude', ),
- 0x0017: ('GPSDestBearingRef', ),
- 0x0018: ('GPSDestBearing', ),
- 0x0019: ('GPSDestDistanceRef', ),
- 0x001A: ('GPSDestDistance', ),
- 0x001B: ('GPSProcessingMethod', ),
- 0x001C: ('GPSAreaInformation', ),
- 0x001D: ('GPSDate', ),
- 0x001E: ('GPSDifferential', ),
- }
-
-# Ignore these tags when quick processing
-# 0x927C is MakerNote Tags
-# 0x9286 is user comment
-IGNORE_TAGS=(0x9286, 0x927C)
-
-# http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
-def nikon_ev_bias(seq):
- # First digit seems to be in steps of 1/6 EV.
- # Does the third value mean the step size? It is usually 6,
- # but it is 12 for the ExposureDifference.
- #
- # Check for an error condition that could cause a crash.
- # This only happens if something has gone really wrong in
- # reading the Nikon MakerNote.
- if len( seq ) < 4 : return ""
- #
- if seq == [252, 1, 6, 0]:
- return "-2/3 EV"
- if seq == [253, 1, 6, 0]:
- return "-1/2 EV"
- if seq == [254, 1, 6, 0]:
- return "-1/3 EV"
- if seq == [0, 1, 6, 0]:
- return "0 EV"
- if seq == [2, 1, 6, 0]:
- return "+1/3 EV"
- if seq == [3, 1, 6, 0]:
- return "+1/2 EV"
- if seq == [4, 1, 6, 0]:
- return "+2/3 EV"
- # Handle combinations not in the table.
- a = seq[0]
- # Causes headaches for the +/- logic, so special case it.
- if a == 0:
- return "0 EV"
- if a > 127:
- a = 256 - a
- ret_str = "-"
- else:
- ret_str = "+"
- b = seq[2] # Assume third value means the step size
- whole = a / b
- a = a % b
- if whole != 0:
- ret_str = ret_str + str(whole) + " "
- if a == 0:
- ret_str = ret_str + "EV"
- else:
- r = Ratio(a, b)
- ret_str = ret_str + r.__repr__() + " EV"
- return ret_str
-
-# Nikon E99x MakerNote Tags
-MAKERNOTE_NIKON_NEWER_TAGS={
- 0x0001: ('MakernoteVersion', make_string), # Sometimes binary
- 0x0002: ('ISOSetting', make_string),
- 0x0003: ('ColorMode', ),
- 0x0004: ('Quality', ),
- 0x0005: ('Whitebalance', ),
- 0x0006: ('ImageSharpening', ),
- 0x0007: ('FocusMode', ),
- 0x0008: ('FlashSetting', ),
- 0x0009: ('AutoFlashMode', ),
- 0x000B: ('WhiteBalanceBias', ),
- 0x000C: ('WhiteBalanceRBCoeff', ),
- 0x000D: ('ProgramShift', nikon_ev_bias),
- # Nearly the same as the other EV vals, but step size is 1/12 EV (?)
- 0x000E: ('ExposureDifference', nikon_ev_bias),
- 0x000F: ('ISOSelection', ),
- 0x0011: ('NikonPreview', ),
- 0x0012: ('FlashCompensation', nikon_ev_bias),
- 0x0013: ('ISOSpeedRequested', ),
- 0x0016: ('PhotoCornerCoordinates', ),
- # 0x0017: Unknown, but most likely an EV value
- 0x0018: ('FlashBracketCompensationApplied', nikon_ev_bias),
- 0x0019: ('AEBracketCompensationApplied', ),
- 0x001A: ('ImageProcessing', ),
- 0x001B: ('CropHiSpeed', ),
- 0x001D: ('SerialNumber', ), # Conflict with 0x00A0 ?
- 0x001E: ('ColorSpace', ),
- 0x001F: ('VRInfo', ),
- 0x0020: ('ImageAuthentication', ),
- 0x0022: ('ActiveDLighting', ),
- 0x0023: ('PictureControl', ),
- 0x0024: ('WorldTime', ),
- 0x0025: ('ISOInfo', ),
- 0x0080: ('ImageAdjustment', ),
- 0x0081: ('ToneCompensation', ),
- 0x0082: ('AuxiliaryLens', ),
- 0x0083: ('LensType', ),
- 0x0084: ('LensMinMaxFocalMaxAperture', ),
- 0x0085: ('ManualFocusDistance', ),
- 0x0086: ('DigitalZoomFactor', ),
- 0x0087: ('FlashMode',
- {0x00: 'Did Not Fire',
- 0x01: 'Fired, Manual',
- 0x07: 'Fired, External',
- 0x08: 'Fired, Commander Mode ',
- 0x09: 'Fired, TTL Mode'}),
- 0x0088: ('AFFocusPosition',
- {0x0000: 'Center',
- 0x0100: 'Top',
- 0x0200: 'Bottom',
- 0x0300: 'Left',
- 0x0400: 'Right'}),
- 0x0089: ('BracketingMode',
- {0x00: 'Single frame, no bracketing',
- 0x01: 'Continuous, no bracketing',
- 0x02: 'Timer, no bracketing',
- 0x10: 'Single frame, exposure bracketing',
- 0x11: 'Continuous, exposure bracketing',
- 0x12: 'Timer, exposure bracketing',
- 0x40: 'Single frame, white balance bracketing',
- 0x41: 'Continuous, white balance bracketing',
- 0x42: 'Timer, white balance bracketing'}),
- 0x008A: ('AutoBracketRelease', ),
- 0x008B: ('LensFStops', ),
- 0x008C: ('NEFCurve1', ), # ExifTool calls this 'ContrastCurve'
- 0x008D: ('ColorMode', ),
- 0x008F: ('SceneMode', ),
- 0x0090: ('LightingType', ),
- 0x0091: ('ShotInfo', ), # First 4 bytes are a version number in ASCII
- 0x0092: ('HueAdjustment', ),
- # ExifTool calls this 'NEFCompression', should be 1-4
- 0x0093: ('Compression', ),
- 0x0094: ('Saturation',
- {-3: 'B&W',
- -2: '-2',
- -1: '-1',
- 0: '0',
- 1: '1',
- 2: '2'}),
- 0x0095: ('NoiseReduction', ),
- 0x0096: ('NEFCurve2', ), # ExifTool calls this 'LinearizationTable'
- 0x0097: ('ColorBalance', ), # First 4 bytes are a version number in ASCII
- 0x0098: ('LensData', ), # First 4 bytes are a version number in ASCII
- 0x0099: ('RawImageCenter', ),
- 0x009A: ('SensorPixelSize', ),
- 0x009C: ('Scene Assist', ),
- 0x009E: ('RetouchHistory', ),
- 0x00A0: ('SerialNumber', ),
- 0x00A2: ('ImageDataSize', ),
- # 00A3: unknown - a single byte 0
- # 00A4: In NEF, looks like a 4 byte ASCII version number ('0200')
- 0x00A5: ('ImageCount', ),
- 0x00A6: ('DeletedImageCount', ),
- 0x00A7: ('TotalShutterReleases', ),
- # First 4 bytes are a version number in ASCII, with version specific
- # info to follow. Its hard to treat it as a string due to embedded nulls.
- 0x00A8: ('FlashInfo', ),
- 0x00A9: ('ImageOptimization', ),
- 0x00AA: ('Saturation', ),
- 0x00AB: ('DigitalVariProgram', ),
- 0x00AC: ('ImageStabilization', ),
- 0x00AD: ('Responsive AF', ), # 'AFResponse'
- 0x00B0: ('MultiExposure', ),
- 0x00B1: ('HighISONoiseReduction', ),
- 0x00B7: ('AFInfo', ),
- 0x00B8: ('FileInfo', ),
- # 00B9: unknown
- 0x0100: ('DigitalICE', ),
- 0x0103: ('PreviewCompression',
- {1: 'Uncompressed',
- 2: 'CCITT 1D',
- 3: 'T4/Group 3 Fax',
- 4: 'T6/Group 4 Fax',
- 5: 'LZW',
- 6: 'JPEG (old-style)',
- 7: 'JPEG',
- 8: 'Adobe Deflate',
- 9: 'JBIG B&W',
- 10: 'JBIG Color',
- 32766: 'Next',
- 32769: 'Epson ERF Compressed',
- 32771: 'CCIRLEW',
- 32773: 'PackBits',
- 32809: 'Thunderscan',
- 32895: 'IT8CTPAD',
- 32896: 'IT8LW',
- 32897: 'IT8MP',
- 32898: 'IT8BL',
- 32908: 'PixarFilm',
- 32909: 'PixarLog',
- 32946: 'Deflate',
- 32947: 'DCS',
- 34661: 'JBIG',
- 34676: 'SGILog',
- 34677: 'SGILog24',
- 34712: 'JPEG 2000',
- 34713: 'Nikon NEF Compressed',
- 65000: 'Kodak DCR Compressed',
- 65535: 'Pentax PEF Compressed',}),
- 0x0201: ('PreviewImageStart', ),
- 0x0202: ('PreviewImageLength', ),
- 0x0213: ('PreviewYCbCrPositioning',
- {1: 'Centered',
- 2: 'Co-sited'}),
- 0x0010: ('DataDump', ),
- }
-
-MAKERNOTE_NIKON_OLDER_TAGS = {
- 0x0003: ('Quality',
- {1: 'VGA Basic',
- 2: 'VGA Normal',
- 3: 'VGA Fine',
- 4: 'SXGA Basic',
- 5: 'SXGA Normal',
- 6: 'SXGA Fine'}),
- 0x0004: ('ColorMode',
- {1: 'Color',
- 2: 'Monochrome'}),
- 0x0005: ('ImageAdjustment',
- {0: 'Normal',
- 1: 'Bright+',
- 2: 'Bright-',
- 3: 'Contrast+',
- 4: 'Contrast-'}),
- 0x0006: ('CCDSpeed',
- {0: 'ISO 80',
- 2: 'ISO 160',
- 4: 'ISO 320',
- 5: 'ISO 100'}),
- 0x0007: ('WhiteBalance',
- {0: 'Auto',
- 1: 'Preset',
- 2: 'Daylight',
- 3: 'Incandescent',
- 4: 'Fluorescent',
- 5: 'Cloudy',
- 6: 'Speed Light'}),
- }
-
-# decode Olympus SpecialMode tag in MakerNote
-def olympus_special_mode(v):
- a={
- 0: 'Normal',
- 1: 'Unknown',
- 2: 'Fast',
- 3: 'Panorama'}
- b={
- 0: 'Non-panoramic',
- 1: 'Left to right',
- 2: 'Right to left',
- 3: 'Bottom to top',
- 4: 'Top to bottom'}
- if v[0] not in a or v[2] not in b:
- return v
- return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]])
-
-MAKERNOTE_OLYMPUS_TAGS={
- # ah HAH! those sneeeeeaky bastids! this is how they get past the fact
- # that a JPEG thumbnail is not allowed in an uncompressed TIFF file
- 0x0100: ('JPEGThumbnail', ),
- 0x0200: ('SpecialMode', olympus_special_mode),
- 0x0201: ('JPEGQual',
- {1: 'SQ',
- 2: 'HQ',
- 3: 'SHQ'}),
- 0x0202: ('Macro',
- {0: 'Normal',
- 1: 'Macro',
- 2: 'SuperMacro'}),
- 0x0203: ('BWMode',
- {0: 'Off',
- 1: 'On'}),
- 0x0204: ('DigitalZoom', ),
- 0x0205: ('FocalPlaneDiagonal', ),
- 0x0206: ('LensDistortionParams', ),
- 0x0207: ('SoftwareRelease', ),
- 0x0208: ('PictureInfo', ),
- 0x0209: ('CameraID', make_string), # print as string
- 0x0F00: ('DataDump', ),
- 0x0300: ('PreCaptureFrames', ),
- 0x0404: ('SerialNumber', ),
- 0x1000: ('ShutterSpeedValue', ),
- 0x1001: ('ISOValue', ),
- 0x1002: ('ApertureValue', ),
- 0x1003: ('BrightnessValue', ),
- 0x1004: ('FlashMode', ),
- 0x1004: ('FlashMode',
- {2: 'On',
- 3: 'Off'}),
- 0x1005: ('FlashDevice',
- {0: 'None',
- 1: 'Internal',
- 4: 'External',
- 5: 'Internal + External'}),
- 0x1006: ('ExposureCompensation', ),
- 0x1007: ('SensorTemperature', ),
- 0x1008: ('LensTemperature', ),
- 0x100b: ('FocusMode',
- {0: 'Auto',
- 1: 'Manual'}),
- 0x1017: ('RedBalance', ),
- 0x1018: ('BlueBalance', ),
- 0x101a: ('SerialNumber', ),
- 0x1023: ('FlashExposureComp', ),
- 0x1026: ('ExternalFlashBounce',
- {0: 'No',
- 1: 'Yes'}),
- 0x1027: ('ExternalFlashZoom', ),
- 0x1028: ('ExternalFlashMode', ),
- 0x1029: ('Contrast int16u',
- {0: 'High',
- 1: 'Normal',
- 2: 'Low'}),
- 0x102a: ('SharpnessFactor', ),
- 0x102b: ('ColorControl', ),
- 0x102c: ('ValidBits', ),
- 0x102d: ('CoringFilter', ),
- 0x102e: ('OlympusImageWidth', ),
- 0x102f: ('OlympusImageHeight', ),
- 0x1034: ('CompressionRatio', ),
- 0x1035: ('PreviewImageValid',
- {0: 'No',
- 1: 'Yes'}),
- 0x1036: ('PreviewImageStart', ),
- 0x1037: ('PreviewImageLength', ),
- 0x1039: ('CCDScanMode',
- {0: 'Interlaced',
- 1: 'Progressive'}),
- 0x103a: ('NoiseReduction',
- {0: 'Off',
- 1: 'On'}),
- 0x103b: ('InfinityLensStep', ),
- 0x103c: ('NearLensStep', ),
-
- # TODO - these need extra definitions
- # http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
- 0x2010: ('Equipment', ),
- 0x2020: ('CameraSettings', ),
- 0x2030: ('RawDevelopment', ),
- 0x2040: ('ImageProcessing', ),
- 0x2050: ('FocusInfo', ),
- 0x3000: ('RawInfo ', ),
- }
-
-# 0x2020 CameraSettings
-MAKERNOTE_OLYMPUS_TAG_0x2020={
- 0x0100: ('PreviewImageValid',
- {0: 'No',
- 1: 'Yes'}),
- 0x0101: ('PreviewImageStart', ),
- 0x0102: ('PreviewImageLength', ),
- 0x0200: ('ExposureMode',
- {1: 'Manual',
- 2: 'Program',
- 3: 'Aperture-priority AE',
- 4: 'Shutter speed priority AE',
- 5: 'Program-shift'}),
- 0x0201: ('AELock',
- {0: 'Off',
- 1: 'On'}),
- 0x0202: ('MeteringMode',
- {2: 'Center Weighted',
- 3: 'Spot',
- 5: 'ESP',
- 261: 'Pattern+AF',
- 515: 'Spot+Highlight control',
- 1027: 'Spot+Shadow control'}),
- 0x0300: ('MacroMode',
- {0: 'Off',
- 1: 'On'}),
- 0x0301: ('FocusMode',
- {0: 'Single AF',
- 1: 'Sequential shooting AF',
- 2: 'Continuous AF',
- 3: 'Multi AF',
- 10: 'MF'}),
- 0x0302: ('FocusProcess',
- {0: 'AF Not Used',
- 1: 'AF Used'}),
- 0x0303: ('AFSearch',
- {0: 'Not Ready',
- 1: 'Ready'}),
- 0x0304: ('AFAreas', ),
- 0x0401: ('FlashExposureCompensation', ),
- 0x0500: ('WhiteBalance2',
- {0: 'Auto',
- 16: '7500K (Fine Weather with Shade)',
- 17: '6000K (Cloudy)',
- 18: '5300K (Fine Weather)',
- 20: '3000K (Tungsten light)',
- 21: '3600K (Tungsten light-like)',
- 33: '6600K (Daylight fluorescent)',
- 34: '4500K (Neutral white fluorescent)',
- 35: '4000K (Cool white fluorescent)',
- 48: '3600K (Tungsten light-like)',
- 256: 'Custom WB 1',
- 257: 'Custom WB 2',
- 258: 'Custom WB 3',
- 259: 'Custom WB 4',
- 512: 'Custom WB 5400K',
- 513: 'Custom WB 2900K',
- 514: 'Custom WB 8000K', }),
- 0x0501: ('WhiteBalanceTemperature', ),
- 0x0502: ('WhiteBalanceBracket', ),
- 0x0503: ('CustomSaturation', ), # (3 numbers: 1. CS Value, 2. Min, 3. Max)
- 0x0504: ('ModifiedSaturation',
- {0: 'Off',
- 1: 'CM1 (Red Enhance)',
- 2: 'CM2 (Green Enhance)',
- 3: 'CM3 (Blue Enhance)',
- 4: 'CM4 (Skin Tones)'}),
- 0x0505: ('ContrastSetting', ), # (3 numbers: 1. Contrast, 2. Min, 3. Max)
- 0x0506: ('SharpnessSetting', ), # (3 numbers: 1. Sharpness, 2. Min, 3. Max)
- 0x0507: ('ColorSpace',
- {0: 'sRGB',
- 1: 'Adobe RGB',
- 2: 'Pro Photo RGB'}),
- 0x0509: ('SceneMode',
- {0: 'Standard',
- 6: 'Auto',
- 7: 'Sport',
- 8: 'Portrait',
- 9: 'Landscape+Portrait',
- 10: 'Landscape',
- 11: 'Night scene',
- 13: 'Panorama',
- 16: 'Landscape+Portrait',
- 17: 'Night+Portrait',
- 19: 'Fireworks',
- 20: 'Sunset',
- 22: 'Macro',
- 25: 'Documents',
- 26: 'Museum',
- 28: 'Beach&Snow',
- 30: 'Candle',
- 35: 'Underwater Wide1',
- 36: 'Underwater Macro',
- 39: 'High Key',
- 40: 'Digital Image Stabilization',
- 44: 'Underwater Wide2',
- 45: 'Low Key',
- 46: 'Children',
- 48: 'Nature Macro'}),
- 0x050a: ('NoiseReduction',
- {0: 'Off',
- 1: 'Noise Reduction',
- 2: 'Noise Filter',
- 3: 'Noise Reduction + Noise Filter',
- 4: 'Noise Filter (ISO Boost)',
- 5: 'Noise Reduction + Noise Filter (ISO Boost)'}),
- 0x050b: ('DistortionCorrection',
- {0: 'Off',
- 1: 'On'}),
- 0x050c: ('ShadingCompensation',
- {0: 'Off',
- 1: 'On'}),
- 0x050d: ('CompressionFactor', ),
- 0x050f: ('Gradation',
- {'-1 -1 1': 'Low Key',
- '0 -1 1': 'Normal',
- '1 -1 1': 'High Key'}),
- 0x0520: ('PictureMode',
- {1: 'Vivid',
- 2: 'Natural',
- 3: 'Muted',
- 256: 'Monotone',
- 512: 'Sepia'}),
- 0x0521: ('PictureModeSaturation', ),
- 0x0522: ('PictureModeHue?', ),
- 0x0523: ('PictureModeContrast', ),
- 0x0524: ('PictureModeSharpness', ),
- 0x0525: ('PictureModeBWFilter',
- {0: 'n/a',
- 1: 'Neutral',
- 2: 'Yellow',
- 3: 'Orange',
- 4: 'Red',
- 5: 'Green'}),
- 0x0526: ('PictureModeTone',
- {0: 'n/a',
- 1: 'Neutral',
- 2: 'Sepia',
- 3: 'Blue',
- 4: 'Purple',
- 5: 'Green'}),
- 0x0600: ('Sequence', ), # 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
- 0x0601: ('PanoramaMode', ), # (2 numbers: 1. Mode, 2. Shot number)
- 0x0603: ('ImageQuality2',
- {1: 'SQ',
- 2: 'HQ',
- 3: 'SHQ',
- 4: 'RAW'}),
- 0x0901: ('ManometerReading', ),
- }
-
-
-MAKERNOTE_CASIO_TAGS={
- 0x0001: ('RecordingMode',
- {1: 'Single Shutter',
- 2: 'Panorama',
- 3: 'Night Scene',
- 4: 'Portrait',
- 5: 'Landscape'}),
- 0x0002: ('Quality',
- {1: 'Economy',
- 2: 'Normal',
- 3: 'Fine'}),
- 0x0003: ('FocusingMode',
- {2: 'Macro',
- 3: 'Auto Focus',
- 4: 'Manual Focus',
- 5: 'Infinity'}),
- 0x0004: ('FlashMode',
- {1: 'Auto',
- 2: 'On',
- 3: 'Off',
- 4: 'Red Eye Reduction'}),
- 0x0005: ('FlashIntensity',
- {11: 'Weak',
- 13: 'Normal',
- 15: 'Strong'}),
- 0x0006: ('Object Distance', ),
- 0x0007: ('WhiteBalance',
- {1: 'Auto',
- 2: 'Tungsten',
- 3: 'Daylight',
- 4: 'Fluorescent',
- 5: 'Shade',
- 129: 'Manual'}),
- 0x000B: ('Sharpness',
- {0: 'Normal',
- 1: 'Soft',
- 2: 'Hard'}),
- 0x000C: ('Contrast',
- {0: 'Normal',
- 1: 'Low',
- 2: 'High'}),
- 0x000D: ('Saturation',
- {0: 'Normal',
- 1: 'Low',
- 2: 'High'}),
- 0x0014: ('CCDSpeed',
- {64: 'Normal',
- 80: 'Normal',
- 100: 'High',
- 125: '+1.0',
- 244: '+3.0',
- 250: '+2.0'}),
- }
-
-MAKERNOTE_FUJIFILM_TAGS={
- 0x0000: ('NoteVersion', make_string),
- 0x1000: ('Quality', ),
- 0x1001: ('Sharpness',
- {1: 'Soft',
- 2: 'Soft',
- 3: 'Normal',
- 4: 'Hard',
- 5: 'Hard'}),
- 0x1002: ('WhiteBalance',
- {0: 'Auto',
- 256: 'Daylight',
- 512: 'Cloudy',
- 768: 'DaylightColor-Fluorescent',
- 769: 'DaywhiteColor-Fluorescent',
- 770: 'White-Fluorescent',
- 1024: 'Incandescent',
- 3840: 'Custom'}),
- 0x1003: ('Color',
- {0: 'Normal',
- 256: 'High',
- 512: 'Low'}),
- 0x1004: ('Tone',
- {0: 'Normal',
- 256: 'High',
- 512: 'Low'}),
- 0x1010: ('FlashMode',
- {0: 'Auto',
- 1: 'On',
- 2: 'Off',
- 3: 'Red Eye Reduction'}),
- 0x1011: ('FlashStrength', ),
- 0x1020: ('Macro',
- {0: 'Off',
- 1: 'On'}),
- 0x1021: ('FocusMode',
- {0: 'Auto',
- 1: 'Manual'}),
- 0x1030: ('SlowSync',
- {0: 'Off',
- 1: 'On'}),
- 0x1031: ('PictureMode',
- {0: 'Auto',
- 1: 'Portrait',
- 2: 'Landscape',
- 4: 'Sports',
- 5: 'Night',
- 6: 'Program AE',
- 256: 'Aperture Priority AE',
- 512: 'Shutter Priority AE',
- 768: 'Manual Exposure'}),
- 0x1100: ('MotorOrBracket',
- {0: 'Off',
- 1: 'On'}),
- 0x1300: ('BlurWarning',
- {0: 'Off',
- 1: 'On'}),
- 0x1301: ('FocusWarning',
- {0: 'Off',
- 1: 'On'}),
- 0x1302: ('AEWarning',
- {0: 'Off',
- 1: 'On'}),
- }
-
-MAKERNOTE_CANON_TAGS = {
- 0x0006: ('ImageType', ),
- 0x0007: ('FirmwareVersion', ),
- 0x0008: ('ImageNumber', ),
- 0x0009: ('OwnerName', ),
- }
-
-# this is in element offset, name, optional value dictionary format
-MAKERNOTE_CANON_TAG_0x001 = {
- 1: ('Macromode',
- {1: 'Macro',
- 2: 'Normal'}),
- 2: ('SelfTimer', ),
- 3: ('Quality',
- {2: 'Normal',
- 3: 'Fine',
- 5: 'Superfine'}),
- 4: ('FlashMode',
- {0: 'Flash Not Fired',
- 1: 'Auto',
- 2: 'On',
- 3: 'Red-Eye Reduction',
- 4: 'Slow Synchro',
- 5: 'Auto + Red-Eye Reduction',
- 6: 'On + Red-Eye Reduction',
- 16: 'external flash'}),
- 5: ('ContinuousDriveMode',
- {0: 'Single Or Timer',
- 1: 'Continuous'}),
- 7: ('FocusMode',
- {0: 'One-Shot',
- 1: 'AI Servo',
- 2: 'AI Focus',
- 3: 'MF',
- 4: 'Single',
- 5: 'Continuous',
- 6: 'MF'}),
- 10: ('ImageSize',
- {0: 'Large',
- 1: 'Medium',
- 2: 'Small'}),
- 11: ('EasyShootingMode',
- {0: 'Full Auto',
- 1: 'Manual',
- 2: 'Landscape',
- 3: 'Fast Shutter',
- 4: 'Slow Shutter',
- 5: 'Night',
- 6: 'B&W',
- 7: 'Sepia',
- 8: 'Portrait',
- 9: 'Sports',
- 10: 'Macro/Close-Up',
- 11: 'Pan Focus'}),
- 12: ('DigitalZoom',
- {0: 'None',
- 1: '2x',
- 2: '4x'}),
- 13: ('Contrast',
- {0xFFFF: 'Low',
- 0: 'Normal',
- 1: 'High'}),
- 14: ('Saturation',
- {0xFFFF: 'Low',
- 0: 'Normal',
- 1: 'High'}),
- 15: ('Sharpness',
- {0xFFFF: 'Low',
- 0: 'Normal',
- 1: 'High'}),
- 16: ('ISO',
- {0: 'See ISOSpeedRatings Tag',
- 15: 'Auto',
- 16: '50',
- 17: '100',
- 18: '200',
- 19: '400'}),
- 17: ('MeteringMode',
- {3: 'Evaluative',
- 4: 'Partial',
- 5: 'Center-weighted'}),
- 18: ('FocusType',
- {0: 'Manual',
- 1: 'Auto',
- 3: 'Close-Up (Macro)',
- 8: 'Locked (Pan Mode)'}),
- 19: ('AFPointSelected',
- {0x3000: 'None (MF)',
- 0x3001: 'Auto-Selected',
- 0x3002: 'Right',
- 0x3003: 'Center',
- 0x3004: 'Left'}),
- 20: ('ExposureMode',
- {0: 'Easy Shooting',
- 1: 'Program',
- 2: 'Tv-priority',
- 3: 'Av-priority',
- 4: 'Manual',
- 5: 'A-DEP'}),
- 23: ('LongFocalLengthOfLensInFocalUnits', ),
- 24: ('ShortFocalLengthOfLensInFocalUnits', ),
- 25: ('FocalUnitsPerMM', ),
- 28: ('FlashActivity',
- {0: 'Did Not Fire',
- 1: 'Fired'}),
- 29: ('FlashDetails',
- {14: 'External E-TTL',
- 13: 'Internal Flash',
- 11: 'FP Sync Used',
- 7: '2nd("Rear")-Curtain Sync Used',
- 4: 'FP Sync Enabled'}),
- 32: ('FocusMode',
- {0: 'Single',
- 1: 'Continuous'}),
- }
-
-MAKERNOTE_CANON_TAG_0x004 = {
- 7: ('WhiteBalance',
- {0: 'Auto',
- 1: 'Sunny',
- 2: 'Cloudy',
- 3: 'Tungsten',
- 4: 'Fluorescent',
- 5: 'Flash',
- 6: 'Custom'}),
- 9: ('SequenceNumber', ),
- 14: ('AFPointUsed', ),
- 15: ('FlashBias',
- {0xFFC0: '-2 EV',
- 0xFFCC: '-1.67 EV',
- 0xFFD0: '-1.50 EV',
- 0xFFD4: '-1.33 EV',
- 0xFFE0: '-1 EV',
- 0xFFEC: '-0.67 EV',
- 0xFFF0: '-0.50 EV',
- 0xFFF4: '-0.33 EV',
- 0x0000: '0 EV',
- 0x000C: '0.33 EV',
- 0x0010: '0.50 EV',
- 0x0014: '0.67 EV',
- 0x0020: '1 EV',
- 0x002C: '1.33 EV',
- 0x0030: '1.50 EV',
- 0x0034: '1.67 EV',
- 0x0040: '2 EV'}),
- 19: ('SubjectDistance', ),
- }
-
-# extract multibyte integer in Motorola format (little endian)
-def s2n_motorola(str):
- x = 0
- for c in str:
- x = (x << 8) | ord(c)
- return x
-
-# extract multibyte integer in Intel format (big endian)
-def s2n_intel(str):
- x = 0
- y = 0L
- for c in str:
- x = x | (ord(c) << y)
- y = y + 8
- return x
-
-# ratio object that eventually will be able to reduce itself to lowest
-# common denominator for printing
-def gcd(a, b):
- if b == 0:
- return a
- else:
- return gcd(b, a % b)
-
-class Ratio:
- def __init__(self, num, den):
- self.num = num
- self.den = den
-
- def __repr__(self):
- self.reduce()
- if self.den == 1:
- return str(self.num)
- return '%d/%d' % (self.num, self.den)
-
- def reduce(self):
- div = gcd(self.num, self.den)
- if div > 1:
- self.num = self.num / div
- self.den = self.den / div
-
-# for ease of dealing with tags
-class IFD_Tag:
- def __init__(self, printable, tag, field_type, values, field_offset,
- field_length):
- # printable version of data
- self.printable = printable
- # tag ID number
- self.tag = tag
- # field type as index into FIELD_TYPES
- self.field_type = field_type
- # offset of start of field in bytes from beginning of IFD
- self.field_offset = field_offset
- # length of data field in bytes
- self.field_length = field_length
- # either a string or array of data items
- self.values = values
-
- def __str__(self):
- return self.printable
-
- def __repr__(self):
- try:
- s= '(0x%04X) %s=%s @ %d' % (self.tag,
- FIELD_TYPES[self.field_type][2],
- self.printable,
- self.field_offset)
- except:
- s= '(%s) %s=%s @ %s' % (str(self.tag),
- FIELD_TYPES[self.field_type][2],
- self.printable,
- str(self.field_offset))
- return s
-
-# class that handles an EXIF header
-class EXIF_header:
- def __init__(self, file, endian, offset, fake_exif, strict, debug=0):
- self.file = file
- self.endian = endian
- self.offset = offset
- self.fake_exif = fake_exif
- self.strict = strict
- self.debug = debug
- self.tags = {}
-
- # convert slice to integer, based on sign and endian flags
- # usually this offset is assumed to be relative to the beginning of the
- # start of the EXIF information. For some cameras that use relative tags,
- # this offset may be relative to some other starting point.
- def s2n(self, offset, length, signed=0):
- self.file.seek(self.offset+offset)
- slice=self.file.read(length)
- if self.endian == 'I':
- val=s2n_intel(slice)
- else:
- val=s2n_motorola(slice)
- # Sign extension ?
- if signed:
- msb=1L << (8*length-1)
- if val & msb:
- val=val-(msb << 1)
- return val
-
- # convert offset to string
- def n2s(self, offset, length):
- s = ''
- for dummy in range(length):
- if self.endian == 'I':
- s = s + chr(offset & 0xFF)
- else:
- s = chr(offset & 0xFF) + s
- offset = offset >> 8
- return s
-
- # return first IFD
- def first_IFD(self):
- return self.s2n(4, 4)
-
- # return pointer to next IFD
- def next_IFD(self, ifd):
- entries=self.s2n(ifd, 2)
- next_ifd = self.s2n(ifd+2+12*entries, 4)
- if next_ifd == ifd:
- return 0
- else:
- return next_ifd
-
- # return list of IFDs in header
- def list_IFDs(self):
- i=self.first_IFD()
- a=[]
- while i:
- a.append(i)
- i=self.next_IFD(i)
- return a
-
- # return list of entries in this IFD
- def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0, stop_tag='UNDEF'):
- entries=self.s2n(ifd, 2)
- for i in range(entries):
- # entry is index of start of this IFD in the file
- entry = ifd + 2 + 12 * i
- tag = self.s2n(entry, 2)
-
- # get tag name early to avoid errors, help debug
- tag_entry = dict.get(tag)
- if tag_entry:
- tag_name = tag_entry[0]
- else:
- tag_name = 'Tag 0x%04X' % tag
-
- # ignore certain tags for faster processing
- if not (not detailed and tag in IGNORE_TAGS):
- field_type = self.s2n(entry + 2, 2)
-
- # unknown field type
- if not 0 < field_type < len(FIELD_TYPES):
- if not self.strict:
- continue
- else:
- raise ValueError('unknown type %d in tag 0x%04X' % (field_type, tag))
-
- typelen = FIELD_TYPES[field_type][0]
- count = self.s2n(entry + 4, 4)
- # Adjust for tag id/type/count (2+2+4 bytes)
- # Now we point at either the data or the 2nd level offset
- offset = entry + 8
-
- # If the value fits in 4 bytes, it is inlined, else we
- # need to jump ahead again.
- if count * typelen > 4:
- # offset is not the value; it's a pointer to the value
- # if relative we set things up so s2n will seek to the right
- # place when it adds self.offset. Note that this 'relative'
- # is for the Nikon type 3 makernote. Other cameras may use
- # other relative offsets, which would have to be computed here
- # slightly differently.
- if relative:
- tmp_offset = self.s2n(offset, 4)
- offset = tmp_offset + ifd - 8
- if self.fake_exif:
- offset = offset + 18
- else:
- offset = self.s2n(offset, 4)
-
- field_offset = offset
- if field_type == 2:
- # special case: null-terminated ASCII string
- # XXX investigate
- # sometimes gets too big to fit in int value
- if count != 0: # and count < (2**31): # 2E31 is hardware dependant. --gd
- try:
- self.file.seek(self.offset + offset)
- values = self.file.read(count)
- #print values
- # Drop any garbage after a null.
- values = values.split('\x00', 1)[0]
- except OverflowError:
- values = ''
- else:
- values = []
- signed = (field_type in [6, 8, 9, 10])
-
- # XXX investigate
- # some entries get too big to handle could be malformed
- # file or problem with self.s2n
- if count < 1000:
- for dummy in range(count):
- if field_type in (5, 10):
- # a ratio
- value = Ratio(self.s2n(offset, 4, signed),
- self.s2n(offset + 4, 4, signed))
- else:
- value = self.s2n(offset, typelen, signed)
- values.append(value)
- offset = offset + typelen
- # The test above causes problems with tags that are
- # supposed to have long values! Fix up one important case.
- elif tag_name == 'MakerNote' :
- for dummy in range(count):
- value = self.s2n(offset, typelen, signed)
- values.append(value)
- offset = offset + typelen
- #else :
- # print "Warning: dropping large tag:", tag, tag_name
-
- # now 'values' is either a string or an array
- if count == 1 and field_type != 2:
- printable=str(values[0])
- elif count > 50 and len(values) > 20 :
- printable=str( values[0:20] )[0:-1] + ", ... ]"
- else:
- printable=str(values)
-
- # compute printable version of values
- if tag_entry:
- if len(tag_entry) != 1:
- # optional 2nd tag element is present
- if callable(tag_entry[1]):
- # call mapping function
- printable = tag_entry[1](values)
- else:
- printable = ''
- for i in values:
- # use lookup table for this tag
- printable += tag_entry[1].get(i, repr(i))
-
- self.tags[ifd_name + ' ' + tag_name] = IFD_Tag(printable, tag,
- field_type,
- values, field_offset,
- count * typelen)
- if self.debug:
- print ' debug: %s: %s' % (tag_name,
- repr(self.tags[ifd_name + ' ' + tag_name]))
-
- if tag_name == stop_tag:
- break
-
- # extract uncompressed TIFF thumbnail (like pulling teeth)
- # we take advantage of the pre-existing layout in the thumbnail IFD as
- # much as possible
- def extract_TIFF_thumbnail(self, thumb_ifd):
- entries = self.s2n(thumb_ifd, 2)
- # this is header plus offset to IFD ...
- if self.endian == 'M':
- tiff = 'MM\x00*\x00\x00\x00\x08'
- else:
- tiff = 'II*\x00\x08\x00\x00\x00'
- # ... plus thumbnail IFD data plus a null "next IFD" pointer
- self.file.seek(self.offset+thumb_ifd)
- tiff += self.file.read(entries*12+2)+'\x00\x00\x00\x00'
-
- # fix up large value offset pointers into data area
- for i in range(entries):
- entry = thumb_ifd + 2 + 12 * i
- tag = self.s2n(entry, 2)
- field_type = self.s2n(entry+2, 2)
- typelen = FIELD_TYPES[field_type][0]
- count = self.s2n(entry+4, 4)
- oldoff = self.s2n(entry+8, 4)
- # start of the 4-byte pointer area in entry
- ptr = i * 12 + 18
- # remember strip offsets location
- if tag == 0x0111:
- strip_off = ptr
- strip_len = count * typelen
- # is it in the data area?
- if count * typelen > 4:
- # update offset pointer (nasty "strings are immutable" crap)
- # should be able to say "tiff[ptr:ptr+4]=newoff"
- newoff = len(tiff)
- tiff = tiff[:ptr] + self.n2s(newoff, 4) + tiff[ptr+4:]
- # remember strip offsets location
- if tag == 0x0111:
- strip_off = newoff
- strip_len = 4
- # get original data and store it
- self.file.seek(self.offset + oldoff)
- tiff += self.file.read(count * typelen)
-
- # add pixel strips and update strip offset info
- old_offsets = self.tags['Thumbnail StripOffsets'].values
- old_counts = self.tags['Thumbnail StripByteCounts'].values
- for i in range(len(old_offsets)):
- # update offset pointer (more nasty "strings are immutable" crap)
- offset = self.n2s(len(tiff), strip_len)
- tiff = tiff[:strip_off] + offset + tiff[strip_off + strip_len:]
- strip_off += strip_len
- # add pixel strip to end
- self.file.seek(self.offset + old_offsets[i])
- tiff += self.file.read(old_counts[i])
-
- self.tags['TIFFThumbnail'] = tiff
-
- # decode all the camera-specific MakerNote formats
-
- # Note is the data that comprises this MakerNote. The MakerNote will
- # likely have pointers in it that point to other parts of the file. We'll
- # use self.offset as the starting point for most of those pointers, since
- # they are relative to the beginning of the file.
- #
- # If the MakerNote is in a newer format, it may use relative addressing
- # within the MakerNote. In that case we'll use relative addresses for the
- # pointers.
- #
- # As an aside: it's not just to be annoying that the manufacturers use
- # relative offsets. It's so that if the makernote has to be moved by the
- # picture software all of the offsets don't have to be adjusted. Overall,
- # this is probably the right strategy for makernotes, though the spec is
- # ambiguous. (The spec does not appear to imagine that makernotes would
- # follow EXIF format internally. Once they did, it's ambiguous whether
- # the offsets should be from the header at the start of all the EXIF info,
- # or from the header at the start of the makernote.)
- def decode_maker_note(self):
- note = self.tags['EXIF MakerNote']
-
- # Some apps use MakerNote tags but do not use a format for which we
- # have a description, so just do a raw dump for these.
- #if self.tags.has_key('Image Make'):
- make = self.tags['Image Make'].printable
- #else:
- # make = ''
-
- # model = self.tags['Image Model'].printable # unused
-
- # Nikon
- # The maker note usually starts with the word Nikon, followed by the
- # type of the makernote (1 or 2, as a short). If the word Nikon is
- # not at the start of the makernote, it's probably type 2, since some
- # cameras work that way.
- if 'NIKON' in make:
- if note.values[0:7] == [78, 105, 107, 111, 110, 0, 1]:
- if self.debug:
- print "Looks like a type 1 Nikon MakerNote."
- self.dump_IFD(note.field_offset+8, 'MakerNote',
- dict=MAKERNOTE_NIKON_OLDER_TAGS)
- elif note.values[0:7] == [78, 105, 107, 111, 110, 0, 2]:
- if self.debug:
- print "Looks like a labeled type 2 Nikon MakerNote"
- if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]:
- raise ValueError("Missing marker tag '42' in MakerNote.")
- # skip the Makernote label and the TIFF header
- self.dump_IFD(note.field_offset+10+8, 'MakerNote',
- dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1)
- else:
- # E99x or D1
- if self.debug:
- print "Looks like an unlabeled type 2 Nikon MakerNote"
- self.dump_IFD(note.field_offset, 'MakerNote',
- dict=MAKERNOTE_NIKON_NEWER_TAGS)
- return
-
- # Olympus
- if make.startswith('OLYMPUS'):
- self.dump_IFD(note.field_offset+8, 'MakerNote',
- dict=MAKERNOTE_OLYMPUS_TAGS)
- # XXX TODO
- #for i in (('MakerNote Tag 0x2020', MAKERNOTE_OLYMPUS_TAG_0x2020),):
- # self.decode_olympus_tag(self.tags[i[0]].values, i[1])
- #return
-
- # Casio
- if 'CASIO' in make or 'Casio' in make:
- self.dump_IFD(note.field_offset, 'MakerNote',
- dict=MAKERNOTE_CASIO_TAGS)
- return
-
- # Fujifilm
- if make == 'FUJIFILM':
- # bug: everything else is "Motorola" endian, but the MakerNote
- # is "Intel" endian
- endian = self.endian
- self.endian = 'I'
- # bug: IFD offsets are from beginning of MakerNote, not
- # beginning of file header
- offset = self.offset
- self.offset += note.field_offset
- # process note with bogus values (note is actually at offset 12)
- self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS)
- # reset to correct values
- self.endian = endian
- self.offset = offset
- return
-
- # Canon
- if make == 'Canon':
- self.dump_IFD(note.field_offset, 'MakerNote',
- dict=MAKERNOTE_CANON_TAGS)
- for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001),
- ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)):
- if i[0] in self.tags:
- self.canon_decode_tag(self.tags[i[0]].values, i[1])
- return
-
-
- # XXX TODO decode Olympus MakerNote tag based on offset within tag
- def olympus_decode_tag(self, value, dict):
- pass
-
- # decode Canon MakerNote tag based on offset within tag
- # see http://www.burren.cx/david/canon.html by David Burren
- def canon_decode_tag(self, value, dict):
- for i in range(1, len(value)):
- x=dict.get(i, ('Unknown', ))
- if self.debug:
- print i, x
- name=x[0]
- if len(x) > 1:
- val=x[1].get(value[i], 'Unknown')
- else:
- val=value[i]
- # it's not a real IFD Tag but we fake one to make everybody
- # happy. this will have a "proprietary" type
- self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None,
- None, None)
-
-# process an image file (expects an open file object)
-# this is the function that has to deal with all the arbitrary nasty bits
-# of the EXIF standard
-def process_file(f, stop_tag='UNDEF', details=True, strict=False, debug=False):
- # yah it's cheesy...
- global detailed
- detailed = details
-
- # by default do not fake an EXIF beginning
- fake_exif = 0
-
- # determine whether it's a JPEG or TIFF
- data = f.read(12)
- if data[0:4] in ['II*\x00', 'MM\x00*']:
- # it's a TIFF file
- f.seek(0)
- endian = f.read(1)
- f.read(1)
- offset = 0
- elif data[0:2] == '\xFF\xD8':
- # it's a JPEG file
- if debug: print "JPEG format recognized data[0:2] == '0xFFD8'."
- base = 2
- while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM', 'Phot'):
- if debug: print "data[2] == 0xxFF data[3]==%x and data[6:10] = %s"%(ord(data[3]),data[6:10])
- length = ord(data[4])*256+ord(data[5])
- if debug: print "Length offset is",length
- f.read(length-8)
- # fake an EXIF beginning of file
- # I don't think this is used. --gd
- data = '\xFF\x00'+f.read(10)
- fake_exif = 1
- if base>2:
- if debug: print "added to base "
- base = base + length + 4 -2
- else:
- if debug: print "added to zero "
- base = length + 4
- if debug: print "Set segment base to",base
-
- # Big ugly patch to deal with APP2 (or other) data coming before APP1
- f.seek(0)
- data = f.read(base+4000) # in theory, this could be insufficient since 64K is the maximum size--gd
- # base = 2
- while 1:
- if debug: print "Segment base 0x%X" % base
- if data[base:base+2]=='\xFF\xE1':
- # APP1
- if debug: print "APP1 at base",hex(base)
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if data[base+4:base+8] == "Exif":
- if debug: print "Decrement base by",2,"to get to pre-segment header (for compatibility with later code)"
- base = base-2
- break
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- elif data[base:base+2]=='\xFF\xE0':
- # APP0
- if debug: print "APP0 at base",hex(base)
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- elif data[base:base+2]=='\xFF\xE2':
- # APP2
- if debug: print "APP2 at base",hex(base)
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- elif data[base:base+2]=='\xFF\xEE':
- # APP14
- if debug: print "APP14 Adobe segment at base",hex(base)
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- print "There is useful EXIF-like data here, but we have no parser for it."
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- elif data[base:base+2]=='\xFF\xDB':
- if debug: print "JPEG image data at base",hex(base),"No more segments are expected."
- # sys.exit(0)
- break
- elif data[base:base+2]=='\xFF\xD8':
- # APP12
- if debug: print "FFD8 segment at base",hex(base)
- if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- elif data[base:base+2]=='\xFF\xEC':
- # APP12
- if debug: print "APP12 XMP (Ducky) or Pictureinfo segment at base",hex(base)
- if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
- if debug: print "Length",hex(ord(data[base+2])), hex(ord(data[base+3]))
- if debug: print "Code",data[base+4:base+8]
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- print "There is useful EXIF-like data here (quality, comment, copyright), but we have no parser for it."
- base=base+ord(data[base+2])*256+ord(data[base+3])+2
- else:
- try:
- if debug: print "Unexpected/unhandled segment type or file content."
- if debug: print "Got",hex(ord(data[base])), hex(ord(data[base+1])),"and", data[4+base:10+base], "instead."
- if debug: print "Increment base by",ord(data[base+2])*256+ord(data[base+3])+2
- except: pass
- try: base=base+ord(data[base+2])*256+ord(data[base+3])+2
- except: pass
-
- f.seek(base+12)
- if data[2+base] == '\xFF' and data[6+base:10+base] == 'Exif':
- # detected EXIF header
- offset = f.tell()
- endian = f.read(1)
- #HACK TEST: endian = 'M'
- elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Ducky':
- # detected Ducky header.
- if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
- offset = f.tell()
- endian = f.read(1)
- elif data[2+base] == '\xFF' and data[6+base:10+base+1] == 'Adobe':
- # detected APP14 (Adobe)
- if debug: print "EXIF-like header (normally 0xFF and code):",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
- offset = f.tell()
- endian = f.read(1)
- else:
- # no EXIF information
- if debug: print "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)"
- if debug: print " but got",hex(ord(data[2+base])) , "and", data[6+base:10+base+1]
- return {}
- else:
- # file format not recognized
- if debug: print "file format not recognized"
- return {}
-
- # deal with the EXIF info we found
- if debug:
- print "Endian format is ",endian
- print {'I': 'Intel', 'M': 'Motorola', '\x01':'Adobe Ducky', 'd':'XMP/Adobe unknown' }[endian], 'format'
- hdr = EXIF_header(f, endian, offset, fake_exif, strict, debug)
- ifd_list = hdr.list_IFDs()
- ctr = 0
- for i in ifd_list:
- if ctr == 0:
- IFD_name = 'Image'
- elif ctr == 1:
- IFD_name = 'Thumbnail'
- thumb_ifd = i
- else:
- IFD_name = 'IFD %d' % ctr
- if debug:
- print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i)
- hdr.dump_IFD(i, IFD_name, stop_tag=stop_tag)
- # EXIF IFD
- exif_off = hdr.tags.get(IFD_name+' ExifOffset')
- if exif_off:
- if debug:
- print ' EXIF SubIFD at offset %d:' % exif_off.values[0]
- hdr.dump_IFD(exif_off.values[0], 'EXIF', stop_tag=stop_tag)
- # Interoperability IFD contained in EXIF IFD
- intr_off = hdr.tags.get('EXIF SubIFD InteroperabilityOffset')
- if intr_off:
- if debug:
- print ' EXIF Interoperability SubSubIFD at offset %d:' \
- % intr_off.values[0]
- hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability',
- dict=INTR_TAGS, stop_tag=stop_tag)
- # GPS IFD
- gps_off = hdr.tags.get(IFD_name+' GPSInfo')
- if gps_off:
- if debug:
- print ' GPS SubIFD at offset %d:' % gps_off.values[0]
- hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS, stop_tag=stop_tag)
- ctr += 1
-
- # extract uncompressed TIFF thumbnail
- thumb = hdr.tags.get('Thumbnail Compression')
- if thumb and thumb.printable == 'Uncompressed TIFF':
- hdr.extract_TIFF_thumbnail(thumb_ifd)
-
- # JPEG thumbnail (thankfully the JPEG data is stored as a unit)
- thumb_off = hdr.tags.get('Thumbnail JPEGInterchangeFormat')
- if thumb_off:
- f.seek(offset+thumb_off.values[0])
- size = hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0]
- hdr.tags['JPEGThumbnail'] = f.read(size)
-
- # deal with MakerNote contained in EXIF IFD
- # (Some apps use MakerNote tags but do not use a format for which we
- # have a description, do not process these).
- if 'EXIF MakerNote' in hdr.tags and 'Image Make' in hdr.tags and detailed:
- hdr.decode_maker_note()
-
- # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
- # since it's not allowed in a uncompressed TIFF IFD
- if 'JPEGThumbnail' not in hdr.tags:
- thumb_off=hdr.tags.get('MakerNote JPEGThumbnail')
- if thumb_off:
- f.seek(offset+thumb_off.values[0])
- hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length)
-
- return hdr.tags
-
-
-# show command line usage
-def usage(exit_status):
- msg = 'Usage: EXIF.py [OPTIONS] file1 [file2 ...]\n'
- msg += 'Extract EXIF information from digital camera image files.\n\nOptions:\n'
- msg += '-q --quick Do not process MakerNotes.\n'
- msg += '-t TAG --stop-tag TAG Stop processing when this tag is retrieved.\n'
- msg += '-s --strict Run in strict mode (stop on errors).\n'
- msg += '-d --debug Run in debug mode (display extra info).\n'
- print msg
- sys.exit(exit_status)
-
-# library test/debug function (dump given files)
-if __name__ == '__main__':
- import sys
- import getopt
-
- # parse command line options/arguments
- try:
- opts, args = getopt.getopt(sys.argv[1:], "hqsdt:v", ["help", "quick", "strict", "debug", "stop-tag="])
- except getopt.GetoptError:
- usage(2)
- if args == []:
- usage(2)
- detailed = True
- stop_tag = 'UNDEF'
- debug = False
- strict = False
- for o, a in opts:
- if o in ("-h", "--help"):
- usage(0)
- if o in ("-q", "--quick"):
- detailed = False
- if o in ("-t", "--stop-tag"):
- stop_tag = a
- if o in ("-s", "--strict"):
- strict = True
- if o in ("-d", "--debug"):
- debug = True
-
- # output info for each file
- for filename in args:
- try:
- file=open(filename, 'rb')
- except:
- print "'%s' is unreadable\n"%filename
- continue
- print filename + ':'
- # get the tags
- data = process_file(file, stop_tag=stop_tag, details=detailed, strict=strict, debug=debug)
- if not data:
- print 'No EXIF information found'
- continue
-
- x=data.keys()
- x.sort()
- for i in x:
- if i in ('JPEGThumbnail', 'TIFFThumbnail'):
- continue
- try:
- print ' %s (%s): %s' % \
- (i, FIELD_TYPES[data[i].field_type][2], data[i].printable)
- except:
- print 'error', i, '"', data[i], '"'
- if 'JPEGThumbnail' in data:
- print 'File has JPEG thumbnail'
- print
-
diff --git a/extlib/exif/LICENSE b/extlib/exif/LICENSE
deleted file mode 100644
index 3bd77141..00000000
--- a/extlib/exif/LICENSE
+++ /dev/null
@@ -1 +0,0 @@
-See top of EXIF.py for license and copyright.
diff --git a/extlib/exif/changes.txt b/extlib/exif/changes.txt
deleted file mode 100644
index d1b18e6c..00000000
--- a/extlib/exif/changes.txt
+++ /dev/null
@@ -1,131 +0,0 @@
-~ EXIF.py Changelog ~
-
-2012-11-30 - Gregory Dudek (date of merge).
-Patches and changes:
- Overflow error fixes added (related to 2**31 size)
- GPS tags added.
-
-2012-09-26 - Ianaré Sévi
-Merge patches:
- Add GPS tags
- Add better endian debug info
-
-2012-06-13 - Ianaré Sévi
-Merge patches:
- Support malformed last IFD by fhats
- Light source, Flash and Metering mode dictionaries update by gryfik
-
-2008-07-31 - Ianaré Sévi
-Wikipedia Commons hunt for suitable test case images,
-testing new code additions.
-
-2008-07-09 - Stephen H. Olson
-Fix a problem with reading MakerNotes out of NEF files.
-Add some more Nikon MakerNote tags.
-
-2008-07-08 - Stephen H. Olson
-An error check for large tags totally borked MakerNotes.
- With Nikon anyway, valid MakerNotes can be pretty big.
-Add error check for a crash caused by nikon_ev_bias being
- called with the wrong args.
-Drop any garbage after a null character in string
- (patch from Andrew McNabb <amcnabb@google.com>).
-
-2008-02-12 - Ianaré Sévi
-Fix crash on invalid MakerNote
-Fix crash on huge Makernote (temp fix)
-Add printIM tag 0xC4A5, needs decoding info
-Add 0x9C9B-F range of tags
-Add a bunch of tag definitions from:
- http://owl.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html
-Add 'strict' variable and command line option
-
-2008-01-18 - Gunter Ohrner
-Add 'GPSDate' tag
-
-2007-12-12 - Ianaré Sévi
-Fix quick option on certain image types
-Add note on tag naming in documentation
-
-2007-11-30 - Ianaré Sévi
-Changed -s option to -t
-Put changelog into separate file
-
-2007-10-28 - Ianaré Sévi
-Merged changes from MoinMoin:ReimarBauer
-Added command line option for debug, stop
-processing on tag.
-
-2007-09-27 - Ianaré Sévi
-Add some Olympus Makernote tags.
-
-2007-09-26 - Stephen H. Olson
-Don't error out on invalid Olympus 'SpecialMode'.
-Add a few more Olympus/Minolta tags.
-
-2007-09-22 - Stephen H. Olson
-Don't error on invalid string
-Improved Nikon MakerNote support
-
-2007-05-03 - Martin Stone <mj_stone@users.sourceforge.net>
-Fix for inverted detailed flag and Photoshop header
-
-2007-03-24 - Ianaré Sévi
-Can now ignore MakerNotes Tags for faster processing.
-
-2007-01-18 - Ianaré Sévi <ianare@gmail.com>
-Fixed a couple errors and assuming maintenance of the library.
-
-2006-08-04 MoinMoin:ReimarBauer
-Added an optional parameter name to process_file and dump_IFD. Using this parameter the
-loop is breaked after that tag_name is processed.
-some PEP8 changes
-
----------------------------- original notices -------------------------
-
-Contains code from "exifdump.py" originally written by Thierry Bousch
-<bousch@topo.math.u-psud.fr> and released into the public domain.
-
-Updated and turned into general-purpose library by Gene Cash
-
-Patch Contributors:
-* Simon J. Gerraty <sjg@crufty.net>
-s2n fix & orientation decode
-* John T. Riedl <riedl@cs.umn.edu>
-Added support for newer Nikon type 3 Makernote format for D70 and some
-other Nikon cameras.
-* Joerg Schaefer <schaeferj@gmx.net>
-Fixed subtle bug when faking an EXIF header, which affected maker notes
-using relative offsets, and a fix for Nikon D100.
-
-1999-08-21 TB Last update by Thierry Bousch to his code.
-
-2002-01-17 CEC Discovered code on web.
- Commented everything.
- Made small code improvements.
- Reformatted for readability.
-
-2002-01-19 CEC Added ability to read TIFFs and JFIF-format JPEGs.
- Added ability to extract JPEG formatted thumbnail.
- Added ability to read GPS IFD (not tested).
- Converted IFD data structure to dictionaries indexed by
- tag name.
- Factored into library returning dictionary of IFDs plus
- thumbnail, if any.
-
-2002-01-20 CEC Added MakerNote processing logic.
- Added Olympus MakerNote.
- Converted data structure to single-level dictionary, avoiding
- tag name collisions by prefixing with IFD name. This makes
- it much easier to use.
-2002-01-23 CEC Trimmed nulls from end of string values.
-
-2002-01-25 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote.
-
-2002-01-26 CEC Added ability to extract TIFF thumbnails.
- Added Nikon, Fujifilm, Casio MakerNotes.
-
-2003-11-30 CEC Fixed problem with canon_decode_tag() not creating an
- IFD_Tag() object.
-
-2004-02-15 CEC Finally fixed bit shift warning by converting Y to 0L.
diff --git a/extlib/skeleton b/extlib/skeleton
new file mode 160000
+Subproject 7ab682091d1032035cfcb668e6bd4b465bfa467
diff --git a/mediagoblin.ini b/mediagoblin.ini
index 19c3e4b0..4f94b6e4 100644
--- a/mediagoblin.ini
+++ b/mediagoblin.ini
@@ -35,7 +35,7 @@ allow_reporting = true
## If you want the terms of service displayed, you can uncomment this
# show_tos = true
-user_privilege_scheme= "uploader,commenter,reporter"
+user_privilege_scheme = "uploader,commenter,reporter"
[storage:queuestore]
base_dir = %(here)s/user_dev/media/queue
@@ -52,5 +52,3 @@ base_url = /mgoblin_media/
[[mediagoblin.plugins.geolocation]]
[[mediagoblin.plugins.basic_auth]]
[[mediagoblin.media_types.image]]
-#-> uncomment below to enable blog and run ./bin/gmg dbudate
-#[[mediagoblin.media_types.blog]]
diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py
index 191a2b9d..39df85af 100644
--- a/mediagoblin/auth/tools.py
+++ b/mediagoblin/auth/tools.py
@@ -150,7 +150,7 @@ def register_user(request, register_form):
def get_default_privileges(user):
instance_privilege_scheme = mg_globals.app_config['user_privilege_scheme']
default_privileges = [Privilege.query.filter(
- Privilege.privilege_name==privilege_name).first()
+ Privilege.privilege_name==privilege_name).first()
for privilege_name in instance_privilege_scheme.split(',')]
default_privileges = [privilege for privilege in default_privileges if not privilege == None]
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index a29b481e..ba2b4519 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -90,7 +90,7 @@ upload_limit = integer(default=None)
max_file_size = integer(default=None)
# Privilege scheme
-user_privilege_scheme = string(default="")
+user_privilege_scheme = string(default="uploader,commenter,reporter")
[jinja2]
# Jinja2 supports more directives than the minimum required by mediagoblin.
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index 25ce6642..048cc07c 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -46,6 +46,12 @@ class UserMixin(object):
def bio_html(self):
return cleaned_markdown_conversion(self.bio)
+ def url_for_self(self, urlgen, **kwargs):
+ """Generate a URL for this User's home page."""
+ return urlgen('mediagoblin.user_pages.user_home',
+ user=self.username, **kwargs)
+
+
class GenerateSlugMixin(object):
"""
Mixin to add a generate_slug method to objects.
diff --git a/mediagoblin/gmg_commands/__init__.py b/mediagoblin/gmg_commands/__init__.py
index a1eb599d..9de4130e 100644
--- a/mediagoblin/gmg_commands/__init__.py
+++ b/mediagoblin/gmg_commands/__init__.py
@@ -53,6 +53,10 @@ SUBCOMMAND_MAP = {
'setup': 'mediagoblin.gmg_commands.addmedia:parser_setup',
'func': 'mediagoblin.gmg_commands.addmedia:addmedia',
'help': 'Reprocess media entries'},
+ 'deletemedia': {
+ 'setup': 'mediagoblin.gmg_commands.deletemedia:parser_setup',
+ 'func': 'mediagoblin.gmg_commands.deletemedia:deletemedia',
+ 'help': 'Delete media entries'},
# 'theme': {
# 'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
# 'func': 'mediagoblin.gmg_commands.theme:theme',
diff --git a/mediagoblin/gmg_commands/deletemedia.py b/mediagoblin/gmg_commands/deletemedia.py
new file mode 100644
index 00000000..d08e76cc
--- /dev/null
+++ b/mediagoblin/gmg_commands/deletemedia.py
@@ -0,0 +1,38 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from mediagoblin.gmg_commands import util as commands_util
+
+
+def parser_setup(subparser):
+ subparser.add_argument('media_ids',
+ help='Comma separated list of media IDs to will be deleted.')
+
+
+def deletemedia(args):
+ app = commands_util.setup_app(args)
+
+ media_ids = set(map(int, args.media_ids.split(',')))
+ found_medias = set()
+ filter_ids = app.db.MediaEntry.id.in_(media_ids)
+ medias = app.db.MediaEntry.query.filter(filter_ids).all()
+ for media in medias:
+ found_medias.add(media.id)
+ media.delete()
+ print 'Media ID %d has been deleted.' % media.id
+ for media in media_ids - found_medias:
+ print 'Can\'t find a media with ID %d.' % media
+ print 'Done.'
diff --git a/mediagoblin/media_types/blog/lib.py b/mediagoblin/media_types/blog/lib.py
index 62696b55..b6e3dc06 100644
--- a/mediagoblin/media_types/blog/lib.py
+++ b/mediagoblin/media_types/blog/lib.py
@@ -45,5 +45,12 @@ def get_all_blogposts_of_blog(request, blog, state=None):
blog_posts_list.append(blog_post)
blog_posts_list.reverse()
return blog_posts_list
-
+
+def get_blog_by_slug(request, slug, **kwargs):
+ if slug.startswith('blog_'):
+ blog_id = int(slug[5:])
+ blog = request.db.Blog.query.filter_by(id=blog_id, **kwargs).first()
+ else:
+ blog = request.db.Blog.query.filter_by(slug=slug, **kwargs).first()
+ return blog
diff --git a/mediagoblin/media_types/blog/models.py b/mediagoblin/media_types/blog/models.py
index 7c55e359..0e1ddf97 100644
--- a/mediagoblin/media_types/blog/models.py
+++ b/mediagoblin/media_types/blog/models.py
@@ -34,6 +34,7 @@ class BlogMixin(GenerateSlugMixin):
def check_slug_used(self, slug):
return check_blog_slug_used(self.author, slug, self.id)
+
class Blog(Base, BlogMixin):
__tablename__ = "mediatype__blogs"
id = Column(Integer, primary_key=True)
@@ -42,7 +43,10 @@ class Blog(Base, BlogMixin):
author = Column(Integer, ForeignKey(User.id), nullable=False, index=True) #similar to uploader
created = Column(DateTime, nullable=False, default=datetime.datetime.now, index=True)
slug = Column(Unicode)
-
+
+ @property
+ def slug_or_id(self):
+ return (self.slug or u'blog_{0}'.format(self.id))
def get_all_blog_posts(self, state=None):
blog_posts = Session.query(MediaEntry).join(BlogPostData)\
diff --git a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_post_listing.html b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_post_listing.html
index 8d7a405e..0013f1a3 100644
--- a/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_post_listing.html
+++ b/mediagoblin/media_types/blog/templates/mediagoblin/blog/blog_post_listing.html
@@ -59,7 +59,7 @@
<br/>
<br/>
{% set blog_about_url = request.urlgen('mediagoblin.media_types.blog.blog_about',
- blog_slug=blog.slug, user=blog_owner_name) %}
+ blog_slug=blog.slug_or_id, user=blog_owner_name) %}
<a style="text-decoration:underline" href="{{ blog_about_url}}">About Blog</a>
<br/>
{{ render_pagination(request, pagination) }}
diff --git a/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html b/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html
index f19a9225..8c16daeb 100644
--- a/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html
+++ b/mediagoblin/media_types/blog/templates/mediagoblin/blog/list_of_blogs.html
@@ -32,14 +32,14 @@
<table id="blogs_list">
{% for blog in blogs %}
{% set others_blog_url = request.urlgen('mediagoblin.media_types.blog.blog_post_listing',
- blog_slug=blog.slug, user=user.username) %}
+ blog_slug=blog.slug_or_id, user=user.username) %}
<tr>
{% if not request.user or request.user.username != user.username%}
<td><a href="{{ others_blog_url }}">{{ blog.title }}</a></td>
{% else %}
{% set my_blog_url = request.urlgen('mediagoblin.media_types.blog.blog-dashboard',
- blog_slug=blog.slug, user=request.user.username) %}
+ blog_slug=blog.slug_or_id, user=request.user.username) %}
<td><a href="{{ my_blog_url }}">{{ blog.title }}</a></td>
{% endif %}
<td>&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;</td>
diff --git a/mediagoblin/media_types/blog/views.py b/mediagoblin/media_types/blog/views.py
index 042881e4..a367bef8 100644
--- a/mediagoblin/media_types/blog/views.py
+++ b/mediagoblin/media_types/blog/views.py
@@ -26,7 +26,9 @@ from mediagoblin import mg_globals
from mediagoblin.media_types.blog import forms as blog_forms
from mediagoblin.media_types.blog.models import Blog, BlogPostData
-from mediagoblin.media_types.blog.lib import may_edit_blogpost, set_blogpost_state, get_all_blogposts_of_blog
+from mediagoblin.media_types.blog.lib import (
+ may_edit_blogpost, set_blogpost_state, get_all_blogposts_of_blog,
+ get_blog_by_slug)
from mediagoblin.messages import add_message, SUCCESS, ERROR
from mediagoblin.decorators import (require_active_login, active_user_from_url,
@@ -91,7 +93,7 @@ def blog_edit(request):
#Blog already exists.
else:
- blog = request.db.Blog.query.filter_by(slug=blog_slug).first()
+ blog = get_blog_by_slug(request, blog_slug)
if not blog:
return render_404(request)
if request.method == 'GET':
@@ -129,8 +131,7 @@ def blogpost_create(request):
if request.method == 'POST' and form.validate():
blog_slug = request.matchdict.get('blog_slug')
- blog = request.db.Blog.query.filter_by(slug=blog_slug,
- author=request.user.id).first()
+ blog = get_blog_by_slug(request, blog_slug, author=request.user.id)
if not blog:
return render_404(request)
@@ -173,7 +174,7 @@ def blogpost_edit(request):
blog_post_slug = request.matchdict.get('blog_post_slug', None)
blogpost = request.db.MediaEntry.query.filter_by(slug=blog_post_slug, uploader=request.user.id).first()
- blog = request.db.Blog.query.filter_by(slug=blog_slug, author=request.user.id).first()
+ blog = get_blog_by_slug(request, blog_slug, author=request.user.id)
if not blogpost or not blog:
return render_404(request)
@@ -222,7 +223,7 @@ def blog_dashboard(request, page, url_user=None):
max_blog_count = config['max_blog_count']
if request.user and (request.user.id == url_user.id or request.user.has_privilege(u'admin')):
if blog_slug:
- blog = blogs.filter(Blog.slug==blog_slug).first()
+ blog = get_blog_by_slug(request, blog_slug)
if not blog:
return render_404(request)
else:
@@ -259,7 +260,7 @@ def blog_post_listing(request, page, url_user=None):
Page, listing all the blog posts of a particular blog.
"""
blog_slug = request.matchdict.get('blog_slug', None)
- blog = request.db.Blog.query.filter_by(slug=blog_slug).first()
+ blog = get_blog_by_slug(request, blog_slug, author=request.user.id)
if not blog:
return render_404(request)
@@ -280,12 +281,10 @@ def blog_post_listing(request, page, url_user=None):
@require_active_login
def draft_view(request):
-
blog_slug = request.matchdict.get('blog_slug', None)
blog_post_slug = request.matchdict.get('blog_post_slug', None)
user = request.matchdict.get('user')
-
- blog = request.db.Blog.query.filter_by(author=request.user.id, slug=blog_slug).first()
+ blog = get_blog_by_slug(request, blog_slug, author=request.user.id)
blogpost = request.db.MediaEntry.query.filter_by(state = u'failed', uploader=request.user.id, slug=blog_post_slug).first()
if not blog or not blogpost:
@@ -308,7 +307,7 @@ def blog_delete(request, **kwargs):
owner_user = request.db.User.query.filter_by(username=url_user).first()
blog_slug = request.matchdict.get('blog_slug', None)
- blog = request.db.Blog.query.filter_by(slug=blog_slug, author=owner_user.id).first()
+ blog = get_blog_by_slug(request, blog_slug, author=owner_user.id)
if not blog:
return render_404(reequest)
@@ -355,7 +354,7 @@ def blog_about_view(request):
url_user = request.matchdict.get('user', None)
user = request.db.User.query.filter_by(username=url_user).first()
- blog = request.db.Blog.query.filter_by(author=user.id, slug=blog_slug).first()
+ blog = get_blog_by_slug(request, blog_slug, author=user.id)
if not user or not blog:
return render_404(request)
diff --git a/mediagoblin/plugins/trim_whitespace/README.rst b/mediagoblin/plugins/trim_whitespace/README.rst
index b55ce35e..db9a0c53 100644
--- a/mediagoblin/plugins/trim_whitespace/README.rst
+++ b/mediagoblin/plugins/trim_whitespace/README.rst
@@ -3,17 +3,22 @@
=======================
Mediagoblin templates are written with 80 char limit for better
-readability. However that means that the html output is very verbose
-containing LOTS of whitespace. This plugin inserts a Middleware that
-filters out whitespace from the returned HTML in the Response() objects.
+readability. However that means that the HTML output is very verbose
+containing *lots* of whitespace. This plugin inserts a middleware that
+filters out whitespace from the returned HTML in the ``Response()``
+objects.
-Simply enable this plugin by putting it somewhere where python can reach it and put it's path into the [plugins] section of your mediagoblin.ini or mediagoblin_local.ini like for example this:
+Simply enable this plugin by putting it somewhere where Python can reach
+it and put it's path into the ``[plugins]`` section of your
+``mediagoblin.ini`` or ``mediagoblin_local.ini`` like for example this:
+
+.. code-block:: ini
[plugins]
[[mediagoblin.plugins.trim_whitespace]]
There is no further configuration required. If this plugin is enabled,
-all text/html documents should not have lots of whitespace in between
+all *text/html* documents should not have lots of whitespace in between
elements, although it does a very naive filtering right now (just keep
the first whitespace and delete all subsequent ones).
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 7b422167..32c6c6cb 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -78,6 +78,11 @@ a.highlight {
color: #fff;
}
+.header_right a {
+ text-decoration: none;
+ color: #fff;
+}
+
em {
font-style: italic;
}
@@ -105,26 +110,26 @@ input, textarea {
/* website structure */
-.container {
- margin: auto;
- width: 96%;
- max-width: 940px;
-}
-
header {
width: 100%;
- max-width: 940px;
- margin-left: auto;
- margin-right: auto;
padding: 0;
margin-bottom: 42px;
border-bottom: 1px solid #333;
}
+.header_left {
+ width: 47%;
+ margin: 0 0 0 8px;
+ display: inline-block;
+}
+
.header_right {
- margin: 8px;
+ width: 47%;
+ margin: 8px 8px 4px 0;
display: inline-block;
float: right;
+ text-align: right;
+ line-height: 1.6em;
}
.header_dropdown {
@@ -156,6 +161,10 @@ a.logo {
margin: 6px 8px 6px 0;
}
+.welcomeimage {
+ float: right;
+}
+
.fine_print {
font-size: 0.8em;
}
@@ -176,29 +185,39 @@ footer {
clear: both;
}
-.media_pane {
- width: 640px;
- margin-left: 0px;
+.thumb_gallery {
+ margin-left: 10px;
margin-right: 10px;
- float: left;
}
-.media_sidebar {
- width: 280px;
- margin-left: 10px;
- float: left;
+.profile_showcase .thumb_gallery {
+ margin-left: 0;
+ margin-right: 0;
}
-.profile_sidebar {
- width: 340px;
- margin-right: 10px;
- float: left;
+.media_image_container {
+ display: flex;
+ justify-content: center;
}
-.profile_showcase {
- width: 580px;
- margin-left: 10px;
- float: left;
+.media_image {
+ max-width: 100%;
+}
+
+.media_pane {
+/* in place for possible future wide view */
+/* border-bottom: 1px solid #333333;*/
+}
+
+.media_sidebar {
+/* in place for possible future wide view */
+/* border-left: 1px solid #333333;*/
+/* padding-left: 1em;*/
+/* padding-top: 1em;*/
+}
+
+.media_comments {
+ padding-top: 1em;
}
/* common website elements */
@@ -224,6 +243,17 @@ footer {
color: #283F35;
}
+.button_info {
+ background-color: #508BB5;
+ border-color: #5899C7 #437699 #427496;
+ color: #C3C3C3;
+}
+
+.button_warning {
+ background-color: #8A2D2D;
+ border-color: #913030 #451717 #431212;
+ color: #C3C3C3;
+}
.button_form {
min-width: 99px;
@@ -248,10 +278,28 @@ text-align: center;
padding-top: 70px;
}
+.no_background {
+ background-image: none;
+ height: 0;
+ padding-top: 0;
+ display: inline-block;
+}
+
.right_align {
float: right;
}
+.left_align {
+ float: right;
+}
+
+.pull-right {
+ float: right !important;
+}
+.pull-left {
+ float: left !important;
+}
+
.clear {
clear: both;
display: block;
@@ -266,13 +314,29 @@ text-align: center;
}
.media_sidebar h3 {
- font-size: 1em;
- margin: 0 0 5px;
- border: none;
+ font-size: 1em;
+ margin: 0 0 5px;
+ border: none;
}
.media_sidebar p {
- margin-left: 8px;
+ margin-left: 8px;
+}
+
+.alpha {
+ margin-left:0;
+}
+
+.omega {
+ margin-right:0;
+}
+
+.head {
+ margin-top:0;
+}
+
+.foot {
+ margin-bottom:0;
}
/* forms */
@@ -339,7 +403,7 @@ text-align: center;
border-top: 6px dashed #D49086
}
-.form_field_input input, .form_field_input textarea {
+/*.form_field_input input,*/ .form_field_input textarea {
width: 100%;
}
@@ -555,44 +619,49 @@ img.media_icon {
/* EXIF information */
-#exif_content h3 {
- border-bottom: 1px solid #333;
+#exif_content {
+ padding-bottom: 20px;
}
#exif_camera_information {
- margin-bottom: 20px;
+ margin-bottom: 20px;
+ margin-left: 8px;
+}
+
+#exif_additional_info_button {
+ margin-left: 8px;
}
#exif_additional_info {
- display: none;
+ display: none;
+ margin-left: 8px;
}
#exif_additional_info table {
- font-size: 11px;
- margin-top: 10px;
+ font-size: 11px;
+ margin-top: 10px;
}
#exif_additional_info td {
- vertical-align: top;
- padding-bottom: 5px;
+ vertical-align: top;
+ padding-bottom: 5px;
}
#exif_content .col1 {
- padding-right: 20px;
+ padding-right: 20px;
}
#exif_additional_info table tr {
- margin-bottom: 10px;
+ margin-bottom: 10px;
}
/* navigation */
.navigation {
- float: right;
}
.navigation_button {
- width: 135px;
+ width: 48%;
display: inline-block;
text-align: center;
background-color: #1d1d1d;
@@ -605,7 +674,7 @@ img.media_icon {
}
.navigation_left {
- margin-right: 6px;
+ margin-right: 3px;
}
/* messages */
@@ -673,7 +742,8 @@ table.admin_panel {
}
table.admin_side_panel {
- width: 60%
+ width: 90%;
+ margin-bottom: 10px;
}
table.admin_panel th, table.admin_side_panel th {
@@ -754,24 +824,16 @@ pre {
}
/* Media queries and other responsivisivity */
-@media screen and (max-width: 940px) {
- .media_pane {
- width: 100%;
- margin: 0px;
- }
-
- .media_sidebar {
- width: 100%;
- margin: 0px;
- }
-
+/* initial GMG max 940 */
+@media screen and (max-width: 960px) {
+
img.media_image {
- width: 100%;
- display: inline;
+ max-width: 100%;
+/* display: inline;*/
}
.media_thumbnail {
- width: 21%;
+/* width: 21%;*/
}
.profile_sidebar {
@@ -784,26 +846,11 @@ pre {
margin: 0px;
}
- .navigation {
- float: none;
- }
-
- .navigation_button {
- width: 49%;
- float: right;
- }
-
- .navigation_left {
- margin-right: 0;
- float: left;
- }
-
.navigation {
float: none;
}
.navigation_button {
- width: 49%;
float: right;
padding: 10px 0 14px;
}
@@ -814,59 +861,75 @@ pre {
}
.button_action, .button_action_highlight, .button_form {
- padding: 9px 14px;
+ padding: 5px 14px;
+ margin-bottom: 0.5em;
}
-
- header {
- text-align: center;
+
+}
+/* desktop resolutions */
+@media screen and (min-width: 960px) {
+ .container .three.columns.media_thumbnail {
+ width:180px;
+ margin-left:3px;
+ margin-right:3px;
}
-
- .header_right {
- margin-right: 2%;
- float: none;
+}
+/* Tablet Portrait size to standard 960 (devices and browsers) */
+@media only screen and (min-width: 768px) and (max-width: 959px) {
+ .container .three.columns.media_thumbnail {
+ width:147px;
+ margin-left:2px;
+ margin-right:2px;
}
-
- a.logo {
- margin-left: 2%;
+ .media_thumbnail.thumb_entry img {
+ margin-left: -16.5px;
+ }
+ .thumb_gallery {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ .navigation_button {
}
}
+/* All Mobile Sizes (devices and browser) */
+@media screen and (max-width: 767px) {
+ .thumb_row {
+ margin-bottom: 0;
+ }
+ .thumb_gallery {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ h1,h2,h3,p {
+ margin-bottom: 10px !important;
+ }
-@media screen and (max-width: 570px) {
- .media_thumbnail {
- width: 29%;
+ header {
+ text-align: center;
}
-}
-@media screen and (max-width: 380px) {
- .media_thumbnail {
- width: 46%;
+ .header_right {
+ text-align: center;
}
-}
-/* Exif display */
-#exif_content h3 {
- border-bottom: 1px solid #333;
-}
-#exif_camera_information {
- margin-bottom: 20px;
-}
+ .welcomeimage {
+ float: none;
+ display: inherit;
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 1em;
+ }
-#exif_additional_info {
- display: none;
-}
-#exif_additional_info table {
- font-size: 11px;
- margin-top: 10px;
-}
-#exif_additional_info td {
- vertical-align: top;
- padding-bottom: 5px;
-}
-#exif_content .col1 {
- padding-right: 20px;
-}
-#exif_additional_info table tr {
- margin-bottom: 10px;
+ .media_sidebar {
+ border-left: none;
+ padding-left: 0;
+ padding-top: 1em;
+ margin-top: 1em;
+ }
+
+ .media_comments {
+ border-bottom: 1px solid #333333;
+ }
}
p.verifier {
diff --git a/mediagoblin/static/css/extlib/skeleton.css b/mediagoblin/static/css/extlib/skeleton.css
new file mode 120000
index 00000000..6ecf4919
--- /dev/null
+++ b/mediagoblin/static/css/extlib/skeleton.css
@@ -0,0 +1 @@
+../../../../extlib/skeleton/stylesheets/skeleton.css \ No newline at end of file
diff --git a/mediagoblin/static/extlib/skeleton b/mediagoblin/static/extlib/skeleton
new file mode 120000
index 00000000..737bfce4
--- /dev/null
+++ b/mediagoblin/static/extlib/skeleton
@@ -0,0 +1 @@
+../../../extlib/skeleton/ \ No newline at end of file
diff --git a/mediagoblin/templates/mediagoblin/banned.html b/mediagoblin/templates/mediagoblin/banned.html
index 0b5a6884..151f2b9c 100644
--- a/mediagoblin/templates/mediagoblin/banned.html
+++ b/mediagoblin/templates/mediagoblin/banned.html
@@ -21,7 +21,7 @@
{% block mediagoblin_content %}
<img class="right_align" src="{{ request.staticdirect('/images/404.png') }}"
- alt="{% trans %}Image of goblin stressing out{% endtrans %}" />
+ alt="{% trans %}Image of goblin stressing out{% endtrans %}" style="max-width:100%;"/>
<h1>{% trans %}You have been banned{% endtrans %}
{% if expiration_date %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index 6d49ff47..b4c7eb8b 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -32,6 +32,8 @@
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/extlib/reset.css') }}"/>
<link rel="stylesheet" type="text/css"
+ href="{{ request.staticdirect('/css/extlib/skeleton.css') }}"/>
+ <link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/base.css') }}"/>
<link rel="shortcut icon"
href="{{ request.staticdirect('/images/goblin.ico') }}" />
@@ -61,110 +63,116 @@
{% include 'mediagoblin/bits/body_start.html' %}
{% block mediagoblin_body %}
{% block mediagoblin_header %}
- <header>
- {%- include "mediagoblin/bits/logo.html" -%}
- {% block mediagoblin_header_title %}{% endblock %}
- <div class="header_right">
- {%- if request.user %}
- {% if request.user and
- request.user.has_privilege('active') and
- not request.user.is_banned() %}
+ <div class="container">
+ <header>
+ <div class="row foot">
+ <div class="header_left">
+ {%- include "mediagoblin/bits/logo.html" -%}
+ {% block mediagoblin_header_title %}{% endblock %}
+ </div>
+ <div class="header_right">
+ {%- if request.user %}
+ {% if request.user and
+ request.user.has_privilege('active') and
+ not request.user.is_banned() %}
- {% set notification_count = get_notification_count(request.user.id) %}
- {% if notification_count %}
- <a href="javascript:;" class="notification-gem button_action" title="Notifications">
- {{ notification_count }}</a>
+ {% set notification_count = get_notification_count(request.user.id) %}
+ {% if notification_count %}
+ <a href="javascript:;" class="notification-gem button_action button_info" title="Notifications">
+ {{ notification_count }}</a>
+ {% endif %}
+ <a href="javascript:;" class="button_action header_dropdown_down">&#9660;</a>
+ <a href="javascript:;" class="button_action header_dropdown_up">&#9650;</a>
+ {% elif request.user and not request.user.has_privilege('active') %}
+ {# the following link should only appear when verification is needed #}
+ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
+ user=request.user.username) }}"
+ class="button_action_highlight">
+ {% trans %}Verify your email!{% endtrans %}</a>
+ or <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}log out{% endtrans %}</a>
+ {% elif request.user and request.user.is_banned() %}
+ <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}log out{% endtrans %}</a>
{% endif %}
- <a href="javascript:;" class="button_action header_dropdown_down">&#9660;</a>
- <a href="javascript:;" class="button_action header_dropdown_up">&#9650;</a>
- {% elif request.user and not request.user.has_privilege('active') %}
- {# the following link should only appear when verification is needed #}
- <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
- user=request.user.username) }}"
- class="button_action_highlight">
- {% trans %}Verify your email!{% endtrans %}</a>
- or <a id="logout" href=
- {% if persona is not defined %}
- "{{ request.urlgen('mediagoblin.auth.logout') }}"
- {% else %}
- "javascript:;"
- {% endif %}
- >{% trans %}log out{% endtrans %}</a>
- {% elif request.user and request.user.is_banned() %}
- <a id="logout" href=
- {% if persona is not defined %}
- "{{ request.urlgen('mediagoblin.auth.logout') }}"
- {% else %}
- "javascript:;"
- {% endif %}
- >{% trans %}log out{% endtrans %}</a>
- {% endif %}
- {%- elif auth %}
- <a href=
- {% if persona_auth is defined %}
- "javascript:;" id="persona_login"
- {% else %}
- "{{ request.urlgen('mediagoblin.auth.login') }}"
- {% endif %}
- >
- {%- trans %}Log in{% endtrans -%}
- </a>
- {%- endif %}
- </div>
- <div class="clear"></div>
- {% if request.user and request.user.has_privilege('active') %}
- <div class="header_dropdown">
- <p>
- <span class="dropdown_title">
- {% trans user_url=request.urlgen('mediagoblin.user_pages.user_home',
- user=request.user.username),
- user_name=request.user.username -%}
- <a href="{{ user_url }}">{{ user_name }}</a>'s account
- {%- endtrans %}
- </span>
- &middot;
- <a href="{{ request.urlgen('mediagoblin.edit.account') }}">{%- trans %}Change account settings{% endtrans -%}</a>
- &middot;
- <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
- user=request.user.username) }}">
- {%- trans %}Media processing panel{% endtrans -%}
+ {%- elif auth %}
+ <a href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.login') }}"
+ {% endif %}
+ >
+ {%- trans %}Log in{% endtrans -%}
</a>
- &middot;
- {% template_hook("blog_dashboard_home") %}
- <a id="logout" href=
- {% if persona is not defined %}
- "{{ request.urlgen('mediagoblin.auth.logout') }}"
- {% else %}
- "javascript:;"
- {% endif %}
- >{% trans %}Log out{% endtrans %}</a>
- </p>
- <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
- {%- trans %}Add media{% endtrans -%}
- </a>
- <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.collection') }}">
- {%- trans %}Create new collection{% endtrans -%}
- </a>
- {% if request.user.has_privilege('admin','moderator') %}
+ {%- endif %}
+ </div>
+ <div class="clear"></div>
+ {% if request.user and request.user.has_privilege('active') %}
+ <div class="header_dropdown">
<p>
- <span class="dropdown_title">Moderation powers:</span>
- <a href="{{ request.urlgen('mediagoblin.moderation.media_panel') }}">
- {%- trans %}Media processing panel{% endtrans -%}
- </a>
+ <span class="dropdown_title">
+ {% trans user_url=request.urlgen('mediagoblin.user_pages.user_home',
+ user=request.user.username),
+ user_name=request.user.username -%}
+ <a href="{{ user_url }}">{{ user_name }}</a>'s account
+ {%- endtrans %}
+ </span>
&middot;
- <a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
- {%- trans %}User management panel{% endtrans -%}
- </a>
+ <a href="{{ request.urlgen('mediagoblin.edit.account') }}">{%- trans %}Change account settings{% endtrans -%}</a>
&middot;
- <a href="{{ request.urlgen('mediagoblin.moderation.reports') }}">
- {%- trans %}Report management panel{% endtrans -%}
+ <a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
+ user=request.user.username) }}">
+ {%- trans %}Media processing panel{% endtrans -%}
</a>
+ &middot;
+ {% template_hook("blog_dashboard_home") %}
+ <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}Log out{% endtrans %}</a>
</p>
- {% endif %}
- {% include 'mediagoblin/fragments/header_notifications.html' %}
- </div>
- {% endif %}
- </header>
+ <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
+ {%- trans %}Add media{% endtrans -%}
+ </a>
+ <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.collection') }}">
+ {%- trans %}Create new collection{% endtrans -%}
+ </a>
+ {% if request.user.has_privilege('admin','moderator') %}
+ <p>
+ <span class="dropdown_title">Moderation powers:</span>
+ <a href="{{ request.urlgen('mediagoblin.moderation.media_panel') }}">
+ {%- trans %}Media processing panel{% endtrans -%}
+ </a>
+ &middot;
+ <a href="{{ request.urlgen('mediagoblin.moderation.users') }}">
+ {%- trans %}User management panel{% endtrans -%}
+ </a>
+ &middot;
+ <a href="{{ request.urlgen('mediagoblin.moderation.reports') }}">
+ {%- trans %}Report management panel{% endtrans -%}
+ </a>
+ </p>
+ {% endif %}
+ {% include 'mediagoblin/fragments/header_notifications.html' %}
+ </div>
+ {% endif %}
+ </div><!-- end row -->
+ </header>
+ </div>
{% endblock %}
<div class="container">
{% include 'mediagoblin/bits/above_content.html' %}
diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
index 4e55e618..3d93ea52 100644
--- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
+++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
@@ -19,29 +19,34 @@
{% if request.user %}
<h1>{% trans %}Explore{% endtrans %}</h1>
{% else %}
- <img class="right_align" src="{{ request.staticdirect('/images/home_goblin.png') }}" />
- <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
- <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
- {% if auth %}
- <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
- {% if allow_registration %}
- <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
- <a class="button_action_highlight" href=
- {% if persona_auth is defined %}
- "javascript:;" id="persona_login1"
- {% else %}
- "{{ request.urlgen('mediagoblin.auth.register') }}"
+ <div class="row foot">
+ <div class="eleven columns">
+ <h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
+ <p>{% trans %}This site is running <a href="http://mediagoblin.org">MediaGoblin</a>, an extraordinarily great piece of media hosting software.{% endtrans %}</p>
+ {% if auth %}
+ <p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
+ {% if allow_registration %}
+ <p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
+ <a class="button_action_highlight" href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login1"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.register') }}"
+ {% endif %}
+ {% trans %}
+ >Create an account at this site</a>
+ or
+ {%- endtrans %}
{% endif %}
- {% trans %}
- >Create an account at this site</a>
- or
- {%- endtrans %}
- {% endif %}
- {% endif %}
- {% trans %}
- <a class="button_action" href="http://mediagoblin.readthedocs.org/">Set up MediaGoblin on your own server</a>
- {%- endtrans %}
-
+ {% endif %}
+ {% trans %}
+ <a class="button_action" href="http://mediagoblin.readthedocs.org/">Set up MediaGoblin on your own server</a>
+ {%- endtrans %}
+ </div>
+ <div class="four columns offset-by-one">
+ <img class="welcomeimage" src="/mgoblin_static/images/home_goblin.png">
+ </div>
+ </div>
<div class="clear"></div>
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/edit/attachments.html b/mediagoblin/templates/mediagoblin/edit/attachments.html
index 3fbea3be..d1e33c47 100644
--- a/mediagoblin/templates/mediagoblin/edit/attachments.html
+++ b/mediagoblin/templates/mediagoblin/edit/attachments.html
@@ -57,7 +57,7 @@
<h2>{% trans %}Add attachment{% endtrans %}</h2>
{{- wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
- <a href="{{ media.url_for_self(request.urlgen) }}">
+ <a class="button_action" href="{{ media.url_for_self(request.urlgen) }}">
{%- trans %}Cancel{% endtrans -%}
</a>
<input type="submit" value="{% trans %}Save changes{% endtrans %}"
diff --git a/mediagoblin/templates/mediagoblin/edit/delete_account.html b/mediagoblin/templates/mediagoblin/edit/delete_account.html
index 84d0b580..a7a3c3d2 100644
--- a/mediagoblin/templates/mediagoblin/edit/delete_account.html
+++ b/mediagoblin/templates/mediagoblin/edit/delete_account.html
@@ -41,7 +41,7 @@
'mediagoblin.user_pages.user_home',
user=user.username) }}">{% trans %}Cancel{% endtrans %}</a>
{{ csrf_token }}
- <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" />
+ <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form button_warning" />
</div>
</div>
</form>
diff --git a/mediagoblin/templates/mediagoblin/error.html b/mediagoblin/templates/mediagoblin/error.html
index c16b650f..a8412b24 100644
--- a/mediagoblin/templates/mediagoblin/error.html
+++ b/mediagoblin/templates/mediagoblin/error.html
@@ -21,7 +21,7 @@
{% block mediagoblin_content %}
<img class="right_align" src="{{ request.staticdirect('/images/404.png') }}"
- alt="{% trans %}Image of goblin stressing out{% endtrans %}" />
+ alt="{% trans %}Image of goblin stressing out{% endtrans %}" style="max-width:100%;"/>
<h1>{{ title }}</h1>
<p>{{ err_msg|safe }}</p>
<div class="clear"></div>
diff --git a/mediagoblin/templates/mediagoblin/media_displays/stl.html b/mediagoblin/templates/mediagoblin/media_displays/stl.html
index bc12ce4e..ca0479ce 100644
--- a/mediagoblin/templates/mediagoblin/media_displays/stl.html
+++ b/mediagoblin/templates/mediagoblin/media_displays/stl.html
@@ -80,6 +80,9 @@ window.show_things = function () {
};
</script>
+<div class="media_pane eleven columns">
+<div class="media_image_container">
+
<img
id="perspective"
class="media_image"
@@ -106,6 +109,7 @@ window.show_things = function () {
Image for {{ media_title }}{% endtrans %}" />
<div id="thingy_view" style="width:640px;height:640px;"></div>
+</div>
<div style="padding: 4px;">
<a class="button_action" onclick="show('perspective');">
@@ -133,6 +137,7 @@ window.show_things = function () {
</a>
</div>
+</div>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/moderation/report.html b/mediagoblin/templates/mediagoblin/moderation/report.html
index cedbd49a..5bff0ffa 100644
--- a/mediagoblin/templates/mediagoblin/moderation/report.html
+++ b/mediagoblin/templates/mediagoblin/moderation/report.html
@@ -26,10 +26,12 @@
{% if not report %}
{% trans %}Sorry, no such report found.{% endtrans %}
{% else %}
+ <div class="row">
<a href="{{ request.urlgen('mediagoblin.moderation.reports') }}"
class="return_to_panel button_action"
title="Return to Reports Panel">
{% trans %}Return to Reports Panel{% endtrans %}</a>
+ </div>
<h2>{% trans %}Report{% endtrans %} #{{ report.id }}</h2>
{% if report.is_comment_report() and report.comment %}
@@ -66,7 +68,7 @@
{% elif report.is_media_entry_report() and report.media_entry %}
{% set media_entry = report.media_entry %}
- <div class="media_thumbnail">
+ <div class="three columns media_thumbnail">
<a href="{{ request.urlgen('mediagoblin.user_pages.media_home',
user=media_entry.get_uploader.username,
media=media_entry.slug_or_id) }}">
diff --git a/mediagoblin/templates/mediagoblin/moderation/user.html b/mediagoblin/templates/mediagoblin/moderation/user.html
index 6335ea12..37e7eee9 100644
--- a/mediagoblin/templates/mediagoblin/moderation/user.html
+++ b/mediagoblin/templates/mediagoblin/moderation/user.html
@@ -34,6 +34,16 @@
{% endblock %}
{% block mediagoblin_content -%}
+ <div class="row">
+ <div class="sixteen columns">
+ <a href="{{ request.urlgen('mediagoblin.moderation.users') }}"
+ class="return_to_panel button_action"
+ title="Return to Users Panel">
+ {% trans %}Return to Users Panel{% endtrans %}</a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="six columns">
{# If no user... #}
{% if not user %}
<p>{% trans %}Sorry, no such user found.{% endtrans %}</p>
@@ -52,127 +62,115 @@
{# Active(?) (or at least verified at some point) user, horray! #}
{% else %}
- <a href="{{ request.urlgen('mediagoblin.moderation.users') }}"
- class="return_to_panel button_action"
- title="Return to Users Panel">
- {% trans %}Return to Users Panel{% endtrans %}</a>
- <h1>
- {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%}
- {% if user_banned and user_banned.expiration_date %}
- &mdash; {% trans expiration_date=user_banned.expiration_date -%}
- BANNED until {{ expiration_date }}
- {%- endtrans %}
- {% elif user_banned %}
- &mdash; {% trans %}Banned Indefinitely{% endtrans %}
- {% endif %}
- </h1>
- {% if not user.url and not user.bio %}
- <div class="profile_sidebar empty_space">
+ <h1>
+ {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%}
+ {% if user_banned and user_banned.expiration_date %}
+ &mdash; {% trans expiration_date=user_banned.expiration_date -%}
+ BANNED until {{ expiration_date }}
+ {%- endtrans %}
+ {% elif user_banned %}
+ &mdash; {% trans %}Banned Indefinitely{% endtrans %}
+ {% endif %}
+ </h1>
+ {% if not user.url and not user.bio %}
+ <div class="profile_sidebar empty_space">
+ <p>
+ {% trans -%}
+ This user hasn't filled in their profile (yet).
+ {%- endtrans %}
+ </p>
+ {% else %}
+ <div class="profile_sidebar">
+ {% include "mediagoblin/utils/profile.html" %}
+ {% if request.user and
+ (request.user.id == user.id or request.user.has_privilege('admin')) %}
+ <a href="{{ request.urlgen('mediagoblin.edit.profile',
+ user=user.username) }}">
+ {%- trans %}Edit profile{% endtrans -%}
+ </a>
+ {% endif %}
+ {% endif %}
<p>
- {% trans -%}
- This user hasn't filled in their profile (yet).
- {%- endtrans %}
+ <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list',
+ user=user.username) }}">
+ {%- trans %}Browse collections{% endtrans -%}
+ </a>
</p>
- {% else %}
- <div class="profile_sidebar">
- {% include "mediagoblin/utils/profile.html" %}
- {% if request.user and
- (request.user.id == user.id or request.user.has_privilege('admin')) %}
- <a href="{{ request.urlgen('mediagoblin.edit.profile',
- user=user.username) }}">
- {%- trans %}Edit profile{% endtrans -%}
- </a>
- {% endif %}
- {% endif %}
- <p>
- <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list',
- user=user.username) }}">
- {%- trans %}Browse collections{% endtrans -%}
- </a>
- </p>
- </div>
+ </div>
{% endif %}
+ </div>
{% if user %}
- <h2>
- {%- trans username=user.username -%}
- Active Reports on {{ username }}
- {%- endtrans -%}
- </h2>
- {% if reports.count() %}
- <table class="admin_side_panel">
- <tr>
- <th>{%- trans %}Report ID{% endtrans -%}</th>
- <th>{%- trans %}Reported Content{% endtrans -%}</th>
- <th>{%- trans %}Description of Report{% endtrans -%}</th>
- </tr>
- {% for report in reports %}
- <tr>
- <td>
- <img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" />
- <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
- report_id=report.id) }}">
- {%- trans report_number=report.id -%}
- Report #{{ report_number }}
- {%- endtrans -%}
- </a>
- </td>
- <td>
- {% if report.discriminator == "comment_report" %}
- <a>{%- trans %}Reported Comment{% endtrans -%}</a>
- {% elif report.discriminator == "media_report" %}
- <a>{%- trans %}Reported Media Entry{% endtrans -%}</a>
- {% endif %}
- </td>
- <td>{{ report.report_content[:21] }}
- {% if report.report_content|count >20 %}...{% endif %}</td>
- <td>{%- trans %}Resolve{% endtrans -%}</td>
- </tr>
- {% endfor %}
- <tr><td></td><td></td>
- </table>
- {% else %}
- {%- trans username=user.username -%}
- No active reports filed on {{ username }}
- {%- endtrans -%}
- {% endif %}
- <span class="right_align">
- <a href="{{ request.urlgen(
- 'mediagoblin.moderation.reports') }}?reported_user={{user.id}}">
- {%- trans
- username=user.username %}All reports on {{ username }}{% endtrans %}</a>
- &middot;
- <a href="{{ request.urlgen(
- 'mediagoblin.moderation.reports') }}?reporter={{user.id}}">
- {%- trans username=user.username -%}
- All reports that {{ username }} has filed
- {%- endtrans %}</a>
- </span>
- <span class=clear></span>
+ <div class="ten columns">
+ <h2>
+ {%- trans username=user.username -%}
+ Active Reports on {{ username }}
+ {%- endtrans -%}
+ </h2>
+ {% if reports.count() %}
+ <table class="admin_side_panel">
+ <tr>
+ <th>{%- trans %}Report ID{% endtrans -%}</th>
+ <th>{%- trans %}Reported Content{% endtrans -%}</th>
+ <th>{%- trans %}Description of Report{% endtrans -%}</th>
+ </tr>
+ {% for report in reports %}
+ <tr>
+ <td>
+ <img src="{{ request.staticdirect('/images/icon_clipboard.png') }}" />
+ <a href="{{ request.urlgen('mediagoblin.moderation.reports_detail',
+ report_id=report.id) }}">
+ {%- trans report_number=report.id -%}
+ Report #{{ report_number }}
+ {%- endtrans -%}
+ </a>
+ </td>
+ <td>
+ {% if report.discriminator == "comment_report" %}
+ <a>{%- trans %}Reported Comment{% endtrans -%}</a>
+ {% elif report.discriminator == "media_report" %}
+ <a>{%- trans %}Reported Media Entry{% endtrans -%}</a>
+ {% endif %}
+ </td>
+ <td>{{ report.report_content[:21] }}
+ {% if report.report_content|count >20 %}...{% endif %}</td>
+ <td>{%- trans %}Resolve{% endtrans -%}</td>
+ </tr>
+ {% endfor %}
+ <tr><td></td><td></td>
+ </table>
+ {% else %}
+ {%- trans username=user.username -%}
+ No active reports filed on {{ username }}
+ {%- endtrans -%}
+ {% endif %}
+ <p>
+ <span>
+ <a href="{{ request.urlgen(
+ 'mediagoblin.moderation.reports') }}?reported_user={{user.id}}">
+ {%- trans
+ username=user.username %}All reports on {{ username }}{% endtrans %}</a>
+ &middot;
+ <a href="{{ request.urlgen(
+ 'mediagoblin.moderation.reports') }}?reporter={{user.id}}">
+ {%- trans username=user.username -%}
+ All reports that {{ username }} has filed
+ {%- endtrans %}</a>
+ </span>
+ <span class=clear></span>
+ </p>
+ </div>
+ </div>
+ <div class="row foot">
<h2>{% trans username=user.username -%}
{{ username }}'s Privileges{% endtrans %}</h2>
- <form method=POST action="{{ request.urlgen(
- 'mediagoblin.moderation.ban_or_unban',
- user=user.username) }}" class="right_align">
- {{ csrf_token }}
- {% if request.user.has_privilege('admin') and not user_banned and
- not user.id == request.user.id %}
- {{ wtforms_util.render_divs(ban_form) }}
- <input type=submit class="button_action"
- value="{% trans %}Ban User{% endtrans %}"
- id="ban_user_submit" />
- {% elif request.user.has_privilege('admin') and
- not user.id == request.user.id %}
- <input type=submit class="button_action right_align"
- value="{% trans %}UnBan User{% endtrans %}" />
- {% endif %}
- </form>
+ <div class="six columns">
<form action="{{ request.urlgen('mediagoblin.moderation.give_or_take_away_privilege',
user=user.username) }}"
method=post >
<table class="admin_side_panel">
<tr>
<th>{% trans %}Privilege{% endtrans %}</th>
- <th>{% trans %}User Has Privilege{% endtrans %}</th>
+ <th>{% trans %}Granted{% endtrans %}</th>
</tr>
{% for privilege in privileges %}
<tr>
@@ -202,6 +200,25 @@
{{ csrf_token }}
<input type=hidden name=privilege_name id=hidden_privilege_name />
</form>
+ </div>
+ <div class="five columns">
+ <form method=POST action="{{ request.urlgen(
+ 'mediagoblin.moderation.ban_or_unban',
+ user=user.username) }}">
+ {{ csrf_token }}
+ {% if request.user.has_privilege('admin') and not user_banned and
+ not user.id == request.user.id %}
+ {{ wtforms_util.render_divs(ban_form) }}
+ <input type=submit class="button_action"
+ value="{% trans %}Ban User{% endtrans %}"
+ id="ban_user_submit" />
+ {% elif request.user.has_privilege('admin') and
+ not user.id == request.user.id %}
+ <input type=submit class="button_action"
+ value="{% trans %}UnBan User{% endtrans %}" />
+ {% endif %}
+ </form>
+ </div>
{% endif %}
<script>
$(document).ready(function(){
@@ -214,4 +231,5 @@ $(document).ready(function(){
});
});
</script>
+</div><!--whoami-->
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
index 694eb979..8cfe4b29 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_confirm_delete.html
@@ -19,6 +19,14 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% block title %}
+ {%- trans collection_title=collection.title
+ -%}
+ Delete collection {{ collection_title }}
+ {%- endtrans %} &mdash; {{ super() }}
+
+{% endblock %}
+
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.user_pages.collection_confirm_delete',
@@ -28,7 +36,7 @@
<div class="form_box">
<h1>
{%- trans title=collection.title -%}
- Really delete {{ title }}?
+ Really delete collection: {{ title }}?
{%- endtrans %}
</h1>
@@ -45,7 +53,7 @@
{{- collection.url_for_self(request.urlgen) }}">
{%- trans %}Cancel{% endtrans -%}
</a>
- <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" />
+ <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form button_warning" />
{{ csrf_token }}
</div>
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html b/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
index dc31d90f..84d3eb4c 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_item_confirm_remove.html
@@ -19,6 +19,14 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% block title %}
+ {%- trans media_title=collection_item.get_media_entry.title,
+ collection_title=collection_item.in_collection.title -%}
+ Remove {{ media_title }} from {{ collection_title }}
+ {%- endtrans %} &mdash; {{ super() }}
+
+{% endblock %}
+
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.user_pages.collection_item_confirm_remove',
@@ -51,7 +59,7 @@
{{- collection_item.in_collection.url_for_self(request.urlgen) }}">
{%- trans %}Cancel{% endtrans -%}
</a>
- <input type="submit" value="{% trans %}Remove{% endtrans %}" class="button_form" />
+ <input type="submit" value="{% trans %}Remove{% endtrans %}" class="button_form button_warning" />
{{ csrf_token }}
</div>
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 81e5013e..e01cce5c 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -32,9 +32,9 @@
{% template_hook("media_head") %}
{% endblock mediagoblin_head %}
-
{% block mediagoblin_content %}
- <p class="context">
+<div class="row foot">
+ <p class="eleven columns context">
{%- trans user_url=request.urlgen(
'mediagoblin.user_pages.user_home',
user=media.get_uploader.username),
@@ -42,8 +42,11 @@
❖ Browsing media by <a href="{{user_url}}">{{username}}</a>
{%- endtrans -%}
</p>
- {% include "mediagoblin/utils/prev_next.html" %}
- <div class="media_pane">
+ <div class="five columns">
+ {% include "mediagoblin/utils/prev_next.html" %}
+ </div>
+</div>
+ <div class="media_pane eleven columns">
<div class="media_image_container">
{% block mediagoblin_media %}
{% set display_media = request.app.public_store.file_url(
@@ -67,12 +70,14 @@
{% endif %}
{% endblock %}
</div>
+ <div class="row head foot">
<h2 class="media_title">
{{ media.title }}
</h2>
{% if request.user and
(media.uploader == request.user.id or
request.user.has_privilege('admin')) %}
+ <div class="pull-right" style="padding-top:20px;">
{% set edit_url = request.urlgen('mediagoblin.edit.edit_media',
user= media.get_uploader.username,
media_id=media.id) %}
@@ -80,13 +85,15 @@
{% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
user= media.get_uploader.username,
media_id=media.id) %}
- <a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
-
+ <a class="button_action button_warning" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
+ </div>
{% endif %}
{% autoescape False %}
<p>{{ media.description_html }}</p>
{% endautoescape %}
+ </div>
{% if comments and request.user and request.user.has_privilege('commenter') %}
+ <div class="media_comments">
{% if app_config['allow_comments'] %}
<a
{% if not request.user %}
@@ -160,9 +167,15 @@
</ul>
{{ render_pagination(request, pagination,
media.url_for_self(request.urlgen)) }}
+ {% else %}
+ <div class="empty_space no_background">
{% endif %}
</div>
- <div class="media_sidebar">
+
+ </div>
+
+
+ <div class="five columns media_sidebar">
<h3>{% trans %}Added{% endtrans %}</h3>
<p><span title="{{ media.created.strftime("%I:%M%p %Y-%m-%d") }}">
{%- trans formatted_time=timesince(media.created) -%}
@@ -220,6 +233,7 @@
{% block mediagoblin_sidebar %}
{% endblock %}
- </div>
+ </div><!--end media_sidebar-->
+
<div class="clear"></div>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
index 1d7dcc17..c948ccec 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
@@ -46,7 +46,7 @@
<div class="form_submit_buttons">
{# TODO: This isn't a button really... might do unexpected things :) #}
<a class="button_action" href="{{ media.url_for_self(request.urlgen) }}">{% trans %}Cancel{% endtrans %}</a>
- <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" />
+ <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form button_warning" />
{{ csrf_token }}
</div>
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index 14a67431..51baa9bb 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -41,6 +41,7 @@
{% block mediagoblin_content -%}
+ <div class="six columns">
<h1>
{%- trans username=user.username %}{{ username }}'s profile{% endtrans -%}
</h1>
@@ -81,9 +82,9 @@
</a>
</p>
</div>
-
+ </div><!--end six columns-->
{% if media_entries.count() %}
- <div class="profile_showcase">
+ <div class="ten columns profile_showcase">
{{ object_gallery(request, media_entries, pagination,
pagination_base_url=user_gallery_url, col_number=3) }}
{% include "mediagoblin/utils/object_gallery.html" %}
@@ -101,7 +102,7 @@
</div>
{% else %}
{% if request.user and (request.user.id == user.id) %}
- <div class="profile_showcase empty_space">
+ <div class="ten columns profile_showcase empty_space">
<p>
{% trans -%}
This is where your media will appear, but you don't seem to have added anything yet.
@@ -113,7 +114,7 @@
</a>
</div>
{% else %}
- <div class="profile_showcase empty_space">
+ <div class="ten columns profile_showcase empty_space">
<p>
{% trans -%}
There doesn't seem to be any media here yet...
diff --git a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html
index dfe2ebe2..64b30815 100644
--- a/mediagoblin/templates/mediagoblin/utils/collection_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/collection_gallery.html
@@ -19,15 +19,15 @@
{% from "mediagoblin/utils/pagination.html" import render_pagination %}
{% macro media_grid(request, collection_items, col_number=5) %}
- <table class="thumb_gallery">
+ <div class="thumb_gallery">
{% for row in collection_items|batch(col_number) %}
- <tr class="thumb_row
+ <div class="row thumb_row
{%- if loop.first %} thumb_row_first
{%- elif loop.last %} thumb_row_last{% endif %}">
{% for item in row %}
{% set media_entry = item.get_media_entry %}
{% set entry_url = media_entry.url_for_self(request.urlgen) %}
- <td class="media_thumbnail thumb_entry
+ <div class="three columns media_thumbnail thumb_entry
{%- if loop.first %} thumb_entry_first
{%- elif loop.last %} thumb_entry_last{% endif %}">
<a href="{{ entry_url }}">
@@ -49,11 +49,11 @@
{%- trans %}(remove){% endtrans -%}
</a>
{% endif %}
- </td>
+ </div>
{% endfor %}
- </tr>
+ </div>
{% endfor %}
- </table>
+ </div>
{%- endmacro %}
{#
diff --git a/mediagoblin/templates/mediagoblin/utils/object_gallery.html b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
index d328b552..1b4a15ed 100644
--- a/mediagoblin/templates/mediagoblin/utils/object_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
@@ -19,14 +19,14 @@
{% from "mediagoblin/utils/pagination.html" import render_pagination %}
{% macro media_grid(request, media_entries, col_number=5) %}
- <table class="thumb_gallery">
+ <div class="thumb_gallery">
{% for row in media_entries|batch(col_number) %}
- <tr class="thumb_row
+ <div class="row thumb_row
{%- if loop.first %} thumb_row_first
{%- elif loop.last %} thumb_row_last{% endif %}">
{% for entry in row %}
{% set entry_url = entry.url_for_self(request.urlgen) %}
- <td class="media_thumbnail thumb_entry
+ <div class="three columns media_thumbnail thumb_entry
{%- if loop.first %} thumb_entry_first
{%- elif loop.last %} thumb_entry_last{% endif %}">
<a href="{{ entry_url }}">
@@ -35,11 +35,11 @@
{% if entry.title %}
<a class="thumb_entry_title" href="{{ entry_url }}">{{ entry.title }}</a>
{% endif %}
- </td>
+ </div>
{% endfor %}
- </tr>
+ </div>
{% endfor %}
- </table>
+ </div>
{%- endmacro %}
{#
diff --git a/mediagoblin/tests/__init__.py b/mediagoblin/tests/__init__.py
index cf200791..fbf3fc6c 100644
--- a/mediagoblin/tests/__init__.py
+++ b/mediagoblin/tests/__init__.py
@@ -14,9 +14,50 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import pytest
+
+from mediagoblin.db.models import User
+from mediagoblin.tests.tools import fixture_add_user
+from mediagoblin.tools import template
+
def setup_package():
import warnings
from sqlalchemy.exc import SAWarning
warnings.simplefilter("error", SAWarning)
+
+
+class MGClientTestCase:
+
+ usernames = None
+
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
+ if self.usernames is None:
+ msg = ('The usernames attribute should be overridden '
+ 'in the subclass')
+ raise pytest.skip(msg)
+ for username, options in self.usernames:
+ fixture_add_user(username, **options)
+
+ def user(self, username):
+ return User.query.filter(User.username == username).first()
+
+ def _do_request(self, url, *context_keys, **kwargs):
+ template.clear_test_template_context()
+ response = self.test_app.request(url, **kwargs)
+ context_data = template.TEMPLATE_TEST_CONTEXT
+ for key in context_keys:
+ context_data = context_data[key]
+ return response, context_data
+
+ def do_get(self, url, *context_keys, **kwargs):
+ kwargs['method'] = 'GET'
+ return self._do_request(url, *context_keys, **kwargs)
+
+ def do_post(self, url, *context_keys, **kwargs):
+ kwargs['method'] = 'POST'
+ return self._do_request(url, *context_keys, **kwargs)
diff --git a/mediagoblin/tests/pytest.ini b/mediagoblin/tests/pytest.ini
index e561c074..a823ca23 100644
--- a/mediagoblin/tests/pytest.ini
+++ b/mediagoblin/tests/pytest.ini
@@ -1,2 +1,3 @@
[pytest]
usefixtures = tmpdir pt_fixture_enable_testing
+addopts = --tb=native \ No newline at end of file
diff --git a/mediagoblin/tests/test_exif.py b/mediagoblin/tests/test_exif.py
index c07e24ae..af301818 100644
--- a/mediagoblin/tests/test_exif.py
+++ b/mediagoblin/tests/test_exif.py
@@ -39,7 +39,7 @@ def test_exif_extraction():
gps = get_gps_data(result)
# Do we have the result?
- assert len(result) == 56
+ assert len(result) == 55
# Do we have clean data?
assert len(clean) == 53
diff --git a/mediagoblin/tests/test_modelmethods.py b/mediagoblin/tests/test_modelmethods.py
index 86513c76..ca436c76 100644
--- a/mediagoblin/tests/test_modelmethods.py
+++ b/mediagoblin/tests/test_modelmethods.py
@@ -20,9 +20,11 @@
from mediagoblin.db.base import Session
from mediagoblin.db.models import MediaEntry, User, Privilege
+from mediagoblin.tests import MGClientTestCase
from mediagoblin.tests.tools import fixture_add_user
import mock
+import pytest
class FakeUUID(object):
@@ -30,6 +32,8 @@ class FakeUUID(object):
UUID_MOCK = mock.Mock(return_value=FakeUUID())
+REQUEST_CONTEXT = ['mediagoblin/root.html', 'request']
+
class TestMediaEntrySlugs(object):
def _setup(self):
@@ -204,3 +208,23 @@ def test_media_data_init(test_app):
print repr(obj)
assert obj_in_session == 0
+
+class TestUserUrlForSelf(MGClientTestCase):
+
+ usernames = [(u'lindsay', dict(privileges=[u'active']))]
+
+ def test_url_for_self(self):
+ _, request = self.do_get('/', *REQUEST_CONTEXT)
+
+ assert self.user(u'lindsay').url_for_self(request.urlgen) == '/u/lindsay/'
+
+ def test_url_for_self_not_callable(self):
+ _, request = self.do_get('/', *REQUEST_CONTEXT)
+
+ def fake_urlgen():
+ pass
+
+ with pytest.raises(TypeError) as excinfo:
+ self.user(u'lindsay').url_for_self(fake_urlgen())
+ assert excinfo.errisinstance(TypeError)
+ assert 'object is not callable' in str(excinfo)
diff --git a/mediagoblin/tests/test_submission/COPYING-psycho.txt b/mediagoblin/tests/test_submission/COPYING-psycho.txt
new file mode 100644
index 00000000..693afd22
--- /dev/null
+++ b/mediagoblin/tests/test_submission/COPYING-psycho.txt
@@ -0,0 +1,24 @@
+(C) 2008 Christopher Allan Webber
+
+This character design was originally created by Brian Raddatz for the
+comic "The Misadventures of Okk":
+ http://okk.comicgenesis.com/d/20060414.html
+
+Put into 3d by Christopher Allan Webber (homepage http://dustycloud.org).
+Technically it is not completely the same character. But it is close
+enough to give Brian Raddatz credit, who is an awesome guy.
+
+Under the permission of both the author of this model and the
+character designer, you are free to use this model under the
+Creative Commons Attribution-ShareAlike 3.0 license, which should be
+included in this archive under the filename LICENSE.txt but is also
+available here:
+ http://creativecommons.org/licenses/by-sa/3.0/legalcode
+
+A more plainspoken version is available here:
+ http://creativecommons.org/licenses/by-sa/3.0/deed.en_US
+
+It basically means you can do whatever you want, as long as you
+attribute the original author(s) and share under the same license.
+
+Have fun! \ No newline at end of file
diff --git a/mediagoblin/tests/test_submission/COPYING.txt b/mediagoblin/tests/test_submission/COPYING.txt
index 3818aae4..59716a49 100644
--- a/mediagoblin/tests/test_submission/COPYING.txt
+++ b/mediagoblin/tests/test_submission/COPYING.txt
@@ -1,5 +1,59 @@
-Images located in this directory tree are released under a GPLv3 license
-and CC BY-SA 3.0 license. To the extent possible under law, the author(s)
-have dedicated all copyright and related and neighboring rights to these
-files to the public domain worldwide. These files are distributed without
-any warranty.
+COPYING data for this directory
+
+"Psycho" files (the big* ones)
+==============================
+
+Files included:
+ - big.png
+ - big-fear_of_flight_source.xcf
+ - big-psycho_source.blend
+
+(C) 2008 Christopher Allan Webber
+
+This character design was originally created by Brian Raddatz for the
+comic "The Misadventures of Okk":
+ http://okk.comicgenesis.com/d/20060414.html
+
+Put into 3d by Christopher Allan Webber (homepage http://dustycloud.org).
+Technically it is not completely the same character. But it is close
+enough to give Brian Raddatz credit, who is an awesome guy.
+
+Under the permission of both the author of this model and the
+character designer, you are free to use this model under the
+Creative Commons Attribution-ShareAlike 3.0 license, which should be
+included in this archive under the filename cc-by-sa-3.0.txt but is also
+available here:
+ http://creativecommons.org/licenses/by-sa/3.0/legalcode
+ http://creativecommons.org/licenses/by-sa/3.0/legalcode.txt
+
+A more plainspoken version is available here:
+ http://creativecommons.org/licenses/by-sa/3.0/deed.en_US
+
+It basically means you can do whatever you want, as long as you
+attribute the original author(s) and share under the same license.
+
+Have fun!
+
+
+Gavroche sketch and release art
+===============================
+
+Files included:
+ - good.jpg
+ - good-original_gavroche_sketch_source.xcf
+ - medium.png
+ - medium-robogoblins.xcf
+
+Originally by Christopher Allan Webber from 2011-2012;
+Waved under CC0 1.0. See ../../../licenses/CC0_1.0.txt
+
+
+Others
+======
+
+ - bigblue.png: Probably not copyrightable since it's just a plain
+ blue square.
+ - evil*: Waived under CC0 1.0, but also probably not copyrightable
+ anyway.
+ - this file waived under CC0, for maximum maximum nobody is worrying
+ about anything ;)
diff --git a/mediagoblin/tests/test_submission/big-fear_of_flight_source.xcf b/mediagoblin/tests/test_submission/big-fear_of_flight_source.xcf
new file mode 100644
index 00000000..fe0f4808
--- /dev/null
+++ b/mediagoblin/tests/test_submission/big-fear_of_flight_source.xcf
Binary files differ
diff --git a/mediagoblin/tests/test_submission/big-psycho_source.blend b/mediagoblin/tests/test_submission/big-psycho_source.blend
new file mode 100644
index 00000000..47a2b475
--- /dev/null
+++ b/mediagoblin/tests/test_submission/big-psycho_source.blend
Binary files differ
diff --git a/mediagoblin/tests/test_submission/cc-by-sa-3.0.txt b/mediagoblin/tests/test_submission/cc-by-sa-3.0.txt
new file mode 100644
index 00000000..c90487cb
--- /dev/null
+++ b/mediagoblin/tests/test_submission/cc-by-sa-3.0.txt
@@ -0,0 +1,359 @@
+Creative Commons Legal Code
+
+Attribution-ShareAlike 3.0 Unported
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
+ DAMAGES RESULTING FROM ITS USE.
+
+License
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
+BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
+CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
+CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work and
+ other pre-existing works, such as a translation, adaptation,
+ derivative work, arrangement of music or other alterations of a
+ literary or artistic work, or phonogram or performance and includes
+ cinematographic adaptations or any other form in which the Work may be
+ recast, transformed, or adapted including in any form recognizably
+ derived from the original, except that a work that constitutes a
+ Collection will not be considered an Adaptation for the purpose of
+ this License. For the avoidance of doubt, where the Work is a musical
+ work, performance or phonogram, the synchronization of the Work in
+ timed-relation with a moving image ("synching") will be considered an
+ Adaptation for the purpose of this License.
+ b. "Collection" means a collection of literary or artistic works, such as
+ encyclopedias and anthologies, or performances, phonograms or
+ broadcasts, or other works or subject matter other than works listed
+ in Section 1(f) below, which, by reason of the selection and
+ arrangement of their contents, constitute intellectual creations, in
+ which the Work is included in its entirety in unmodified form along
+ with one or more other contributions, each constituting separate and
+ independent works in themselves, which together are assembled into a
+ collective whole. A work that constitutes a Collection will not be
+ considered an Adaptation (as defined below) for the purposes of this
+ License.
+ c. "Creative Commons Compatible License" means a license that is listed
+ at http://creativecommons.org/compatiblelicenses that has been
+ approved by Creative Commons as being essentially equivalent to this
+ License, including, at a minimum, because that license: (i) contains
+ terms that have the same purpose, meaning and effect as the License
+ Elements of this License; and, (ii) explicitly permits the relicensing
+ of adaptations of works made available under that license under this
+ License or a Creative Commons jurisdiction license with the same
+ License Elements as this License.
+ d. "Distribute" means to make available to the public the original and
+ copies of the Work or Adaptation, as appropriate, through sale or
+ other transfer of ownership.
+ e. "License Elements" means the following high-level license attributes
+ as selected by Licensor and indicated in the title of this License:
+ Attribution, ShareAlike.
+ f. "Licensor" means the individual, individuals, entity or entities that
+ offer(s) the Work under the terms of this License.
+ g. "Original Author" means, in the case of a literary or artistic work,
+ the individual, individuals, entity or entities who created the Work
+ or if no individual or entity can be identified, the publisher; and in
+ addition (i) in the case of a performance the actors, singers,
+ musicians, dancers, and other persons who act, sing, deliver, declaim,
+ play in, interpret or otherwise perform literary or artistic works or
+ expressions of folklore; (ii) in the case of a phonogram the producer
+ being the person or legal entity who first fixes the sounds of a
+ performance or other sounds; and, (iii) in the case of broadcasts, the
+ organization that transmits the broadcast.
+ h. "Work" means the literary and/or artistic work offered under the terms
+ of this License including without limitation any production in the
+ literary, scientific and artistic domain, whatever may be the mode or
+ form of its expression including digital form, such as a book,
+ pamphlet and other writing; a lecture, address, sermon or other work
+ of the same nature; a dramatic or dramatico-musical work; a
+ choreographic work or entertainment in dumb show; a musical
+ composition with or without words; a cinematographic work to which are
+ assimilated works expressed by a process analogous to cinematography;
+ a work of drawing, painting, architecture, sculpture, engraving or
+ lithography; a photographic work to which are assimilated works
+ expressed by a process analogous to photography; a work of applied
+ art; an illustration, map, plan, sketch or three-dimensional work
+ relative to geography, topography, architecture or science; a
+ performance; a broadcast; a phonogram; a compilation of data to the
+ extent it is protected as a copyrightable work; or a work performed by
+ a variety or circus performer to the extent it is not otherwise
+ considered a literary or artistic work.
+ i. "You" means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License with
+ respect to the Work, or who has received express permission from the
+ Licensor to exercise rights under this License despite a previous
+ violation.
+ j. "Publicly Perform" means to perform public recitations of the Work and
+ to communicate to the public those public recitations, by any means or
+ process, including by wire or wireless means or public digital
+ performances; to make available to the public Works in such a way that
+ members of the public may access these Works from a place and at a
+ place individually chosen by them; to perform the Work to the public
+ by any means or process and the communication to the public of the
+ performances of the Work, including by public digital performance; to
+ broadcast and rebroadcast the Work by any means including signs,
+ sounds or images.
+ k. "Reproduce" means to make copies of the Work by any means including
+ without limitation by sound or visual recordings and the right of
+ fixation and reproducing fixations of the Work, including storage of a
+ protected performance or phonogram in digital form or other electronic
+ medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to reduce,
+limit, or restrict any uses free from copyright or rights arising from
+limitations or exceptions that are provided for in connection with the
+copyright protection under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+ Collections, and to Reproduce the Work as incorporated in the
+ Collections;
+ b. to create and Reproduce Adaptations provided that any such Adaptation,
+ including any translation in any medium, takes reasonable steps to
+ clearly label, demarcate or otherwise identify that changes were made
+ to the original Work. For example, a translation could be marked "The
+ original work was translated from English to Spanish," or a
+ modification could indicate "The original work has been modified.";
+ c. to Distribute and Publicly Perform the Work including as incorporated
+ in Collections; and,
+ d. to Distribute and Publicly Perform Adaptations.
+ e. For the avoidance of doubt:
+
+ i. Non-waivable Compulsory License Schemes. In those jurisdictions in
+ which the right to collect royalties through any statutory or
+ compulsory licensing scheme cannot be waived, the Licensor
+ reserves the exclusive right to collect such royalties for any
+ exercise by You of the rights granted under this License;
+ ii. Waivable Compulsory License Schemes. In those jurisdictions in
+ which the right to collect royalties through any statutory or
+ compulsory licensing scheme can be waived, the Licensor waives the
+ exclusive right to collect such royalties for any exercise by You
+ of the rights granted under this License; and,
+ iii. Voluntary License Schemes. The Licensor waives the right to
+ collect royalties, whether individually or, in the event that the
+ Licensor is a member of a collecting society that administers
+ voluntary licensing schemes, via that society, from any exercise
+ by You of the rights granted under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights in
+other media and formats. Subject to Section 8(f), all rights not expressly
+granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made
+subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the terms
+ of this License. You must include a copy of, or the Uniform Resource
+ Identifier (URI) for, this License with every copy of the Work You
+ Distribute or Publicly Perform. You may not offer or impose any terms
+ on the Work that restrict the terms of this License or the ability of
+ the recipient of the Work to exercise the rights granted to that
+ recipient under the terms of the License. You may not sublicense the
+ Work. You must keep intact all notices that refer to this License and
+ to the disclaimer of warranties with every copy of the Work You
+ Distribute or Publicly Perform. When You Distribute or Publicly
+ Perform the Work, You may not impose any effective technological
+ measures on the Work that restrict the ability of a recipient of the
+ Work from You to exercise the rights granted to that recipient under
+ the terms of the License. This Section 4(a) applies to the Work as
+ incorporated in a Collection, but this does not require the Collection
+ apart from the Work itself to be made subject to the terms of this
+ License. If You create a Collection, upon notice from any Licensor You
+ must, to the extent practicable, remove from the Collection any credit
+ as required by Section 4(c), as requested. If You create an
+ Adaptation, upon notice from any Licensor You must, to the extent
+ practicable, remove from the Adaptation any credit as required by
+ Section 4(c), as requested.
+ b. You may Distribute or Publicly Perform an Adaptation only under the
+ terms of: (i) this License; (ii) a later version of this License with
+ the same License Elements as this License; (iii) a Creative Commons
+ jurisdiction license (either this or a later license version) that
+ contains the same License Elements as this License (e.g.,
+ Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible
+ License. If you license the Adaptation under one of the licenses
+ mentioned in (iv), you must comply with the terms of that license. If
+ you license the Adaptation under the terms of any of the licenses
+ mentioned in (i), (ii) or (iii) (the "Applicable License"), you must
+ comply with the terms of the Applicable License generally and the
+ following provisions: (I) You must include a copy of, or the URI for,
+ the Applicable License with every copy of each Adaptation You
+ Distribute or Publicly Perform; (II) You may not offer or impose any
+ terms on the Adaptation that restrict the terms of the Applicable
+ License or the ability of the recipient of the Adaptation to exercise
+ the rights granted to that recipient under the terms of the Applicable
+ License; (III) You must keep intact all notices that refer to the
+ Applicable License and to the disclaimer of warranties with every copy
+ of the Work as included in the Adaptation You Distribute or Publicly
+ Perform; (IV) when You Distribute or Publicly Perform the Adaptation,
+ You may not impose any effective technological measures on the
+ Adaptation that restrict the ability of a recipient of the Adaptation
+ from You to exercise the rights granted to that recipient under the
+ terms of the Applicable License. This Section 4(b) applies to the
+ Adaptation as incorporated in a Collection, but this does not require
+ the Collection apart from the Adaptation itself to be made subject to
+ the terms of the Applicable License.
+ c. If You Distribute, or Publicly Perform the Work or any Adaptations or
+ Collections, You must, unless a request has been made pursuant to
+ Section 4(a), keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i) the
+ name of the Original Author (or pseudonym, if applicable) if supplied,
+ and/or if the Original Author and/or Licensor designate another party
+ or parties (e.g., a sponsor institute, publishing entity, journal) for
+ attribution ("Attribution Parties") in Licensor's copyright notice,
+ terms of service or by other reasonable means, the name of such party
+ or parties; (ii) the title of the Work if supplied; (iii) to the
+ extent reasonably practicable, the URI, if any, that Licensor
+ specifies to be associated with the Work, unless such URI does not
+ refer to the copyright notice or licensing information for the Work;
+ and (iv) , consistent with Ssection 3(b), in the case of an
+ Adaptation, a credit identifying the use of the Work in the Adaptation
+ (e.g., "French translation of the Work by Original Author," or
+ "Screenplay based on original Work by Original Author"). The credit
+ required by this Section 4(c) may be implemented in any reasonable
+ manner; provided, however, that in the case of a Adaptation or
+ Collection, at a minimum such credit will appear, if a credit for all
+ contributing authors of the Adaptation or Collection appears, then as
+ part of these credits and in a manner at least as prominent as the
+ credits for the other contributing authors. For the avoidance of
+ doubt, You may only use the credit required by this Section for the
+ purpose of attribution in the manner set out above and, by exercising
+ Your rights under this License, You may not implicitly or explicitly
+ assert or imply any connection with, sponsorship or endorsement by the
+ Original Author, Licensor and/or Attribution Parties, as appropriate,
+ of You or Your use of the Work, without the separate, express prior
+ written permission of the Original Author, Licensor and/or Attribution
+ Parties.
+ d. Except as otherwise agreed in writing by the Licensor or as may be
+ otherwise permitted by applicable law, if You Reproduce, Distribute or
+ Publicly Perform the Work either by itself or as part of any
+ Adaptations or Collections, You must not distort, mutilate, modify or
+ take other derogatory action in relation to the Work which would be
+ prejudicial to the Original Author's honor or reputation. Licensor
+ agrees that in those jurisdictions (e.g. Japan), in which any exercise
+ of the right granted in Section 3(b) of this License (the right to
+ make Adaptations) would be deemed to be a distortion, mutilation,
+ modification or other derogatory action prejudicial to the Original
+ Author's honor and reputation, the Licensor will waive or not assert,
+ as appropriate, this Section, to the fullest extent permitted by the
+ applicable national law, to enable You to reasonably exercise Your
+ right under Section 3(b) of this License (right to make Adaptations)
+ but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
+LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
+ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
+ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Adaptations or Collections
+ from You under this License, however, will not have their licenses
+ terminated provided such individuals or entities remain in full
+ compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
+ survive any termination of this License.
+ b. Subject to the above terms and conditions, the license granted here is
+ perpetual (for the duration of the applicable copyright in the Work).
+ Notwithstanding the above, Licensor reserves the right to release the
+ Work under different license terms or to stop distributing the Work at
+ any time; provided, however that any such election will not serve to
+ withdraw this License (or any other license that has been, or is
+ required to be, granted under the terms of this License), and this
+ License will continue in full force and effect unless terminated as
+ stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a Collection,
+ the Licensor offers to the recipient a license to the Work on the same
+ terms and conditions as the license granted to You under this License.
+ b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
+ offers to the recipient a license to the original Work on the same
+ terms and conditions as the license granted to You under this License.
+ c. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability of
+ the remainder of the terms of this License, and without further action
+ by the parties to this agreement, such provision shall be reformed to
+ the minimum extent necessary to make such provision valid and
+ enforceable.
+ d. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in writing
+ and signed by the party to be charged with such waiver or consent.
+ e. This License constitutes the entire agreement between the parties with
+ respect to the Work licensed here. There are no understandings,
+ agreements or representations with respect to the Work not specified
+ here. Licensor shall not be bound by any additional provisions that
+ may appear in any communication from You. This License may not be
+ modified without the mutual written agreement of the Licensor and You.
+ f. The rights granted under, and the subject matter referenced, in this
+ License were drafted utilizing the terminology of the Berne Convention
+ for the Protection of Literary and Artistic Works (as amended on
+ September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
+ Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
+ and the Universal Copyright Convention (as revised on July 24, 1971).
+ These rights and subject matter take effect in the relevant
+ jurisdiction in which the License terms are sought to be enforced
+ according to the corresponding provisions of the implementation of
+ those treaty provisions in the applicable national law. If the
+ standard suite of rights granted under applicable copyright law
+ includes additional rights not granted under this License, such
+ additional rights are deemed to be included in the License; this
+ License is not intended to restrict the license of any rights under
+ applicable law.
+
+
+Creative Commons Notice
+
+ Creative Commons is not a party to this License, and makes no warranty
+ whatsoever in connection with the Work. Creative Commons will not be
+ liable to You or any party on any legal theory for any damages
+ whatsoever, including without limitation any general, special,
+ incidental or consequential damages arising in connection to this
+ license. Notwithstanding the foregoing two (2) sentences, if Creative
+ Commons has expressly identified itself as the Licensor hereunder, it
+ shall have all rights and obligations of Licensor.
+
+ Except for the limited purpose of indicating to the public that the
+ Work is licensed under the CCPL, Creative Commons does not authorize
+ the use by either party of the trademark "Creative Commons" or any
+ related trademark or logo of Creative Commons without the prior
+ written consent of Creative Commons. Any permitted use will be in
+ compliance with Creative Commons' then-current trademark usage
+ guidelines, as may be published on its website or otherwise made
+ available upon request from time to time. For the avoidance of doubt,
+ this trademark restriction does not form part of the License.
+
+ Creative Commons may be contacted at http://creativecommons.org/.
diff --git a/mediagoblin/tests/test_submission/good-original_gavroche_sketch_source.xcf b/mediagoblin/tests/test_submission/good-original_gavroche_sketch_source.xcf
new file mode 100644
index 00000000..a64f0963
--- /dev/null
+++ b/mediagoblin/tests/test_submission/good-original_gavroche_sketch_source.xcf
Binary files differ
diff --git a/mediagoblin/tests/test_submission/medium-robogoblins.xcf b/mediagoblin/tests/test_submission/medium-robogoblins.xcf
new file mode 100644
index 00000000..9dc8a57b
--- /dev/null
+++ b/mediagoblin/tests/test_submission/medium-robogoblins.xcf
Binary files differ
diff --git a/mediagoblin/tests/test_util.py b/mediagoblin/tests/test_util.py
index bc14f528..9d9b1c16 100644
--- a/mediagoblin/tests/test_util.py
+++ b/mediagoblin/tests/test_util.py
@@ -77,6 +77,12 @@ def test_slugify():
assert url.slugify(u'a w@lk in the park?') == u'a-w-lk-in-the-park'
assert url.slugify(u'a walk in the par\u0107') == u'a-walk-in-the-parc'
assert url.slugify(u'\u00E0\u0042\u00E7\u010F\u00EB\u0066') == u'abcdef'
+ # Russian
+ assert url.slugify(u'\u043f\u0440\u043e\u0433\u0443\u043b\u043a\u0430 '
+ u'\u0432 \u043f\u0430\u0440\u043a\u0435') == u'progulka-v-parke'
+ # Korean
+ assert (url.slugify(u'\uacf5\uc6d0\uc5d0\uc11c \uc0b0\ucc45') ==
+ u'gongweoneseo-sancaeg')
def test_locale_to_lower_upper():
"""
diff --git a/mediagoblin/themes/airy/assets/css/airy.css b/mediagoblin/themes/airy/assets/css/airy.css
index c4bea5cb..03d2cab8 100644
--- a/mediagoblin/themes/airy/assets/css/airy.css
+++ b/mediagoblin/themes/airy/assets/css/airy.css
@@ -18,6 +18,7 @@ a {
border-width: 1px 1px 2px;
color: #4a4a4a;
font-weight: bold;
+ box-shadow: 0 0 3px #E4E4E4 inset;
}
p.navigation_button {
@@ -33,6 +34,10 @@ header {
margin-right: auto;
}
+.header_right a {
+ color: #4a4a4a;
+}
+
@media screen and (max-width: 940px) {
header {
width: 100%;
@@ -50,6 +55,7 @@ footer {
border-color: #E4E4E4;
border-width: 1px 1px 2px;
padding: 5px 10px;
+ box-shadow: 0 0 3px #E4E4E4 inset;
}
.button_action_highlight, .button_form {
@@ -57,6 +63,21 @@ footer {
background-color: #37AB74;
border-color: #6CAA8E;
border-width: 1px 1px 2px;
+ box-shadow: 0 0 3px #6CAA8E inset;
+}
+
+.button_info {
+ background-color: #9dbed5;
+ border-color: #E4E4E4;
+ color: #C3C3C3;
+ box-shadow: 0 0 3px #49717F inset;
+}
+
+.button_warning {
+ background-color: #d6ae96;
+ border-color: #E4E4E4;
+ color: #4a4a4a;
+ box-shadow: 0 0 3px #7F6859 inset;
}
input, textarea {
@@ -71,6 +92,8 @@ input, textarea {
.media_thumbnail {
background-color: #fff;
+ border-color: #e4e4e4;
+ box-shadow: 0 0 3px #c4c4c4;
}
.media_thumbnail a {
diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py
index 6b3639e8..50f1aabf 100644
--- a/mediagoblin/tools/exif.py
+++ b/mediagoblin/tools/exif.py
@@ -14,10 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-try:
- from EXIF import process_file, Ratio
-except ImportError:
- from mediagoblin.tools.extlib.EXIF import process_file, Ratio
+from exifread import process_file
+from exifread.utils import Ratio
from mediagoblin.processing import BadMediaFail
from mediagoblin.tools.translate import pass_to_ugettext as _
diff --git a/mediagoblin/tools/extlib/EXIF.py b/mediagoblin/tools/extlib/EXIF.py
deleted file mode 120000
index 82a2fb30..00000000
--- a/mediagoblin/tools/extlib/EXIF.py
+++ /dev/null
@@ -1 +0,0 @@
-../../../extlib/exif/EXIF.py \ No newline at end of file
diff --git a/mediagoblin/tools/translate.py b/mediagoblin/tools/translate.py
index f55ce349..257bd791 100644
--- a/mediagoblin/tools/translate.py
+++ b/mediagoblin/tools/translate.py
@@ -42,7 +42,7 @@ def set_available_locales():
"""Set available locales for which we have translations"""
global AVAILABLE_LOCALES
locales=['en', 'en_US'] # these are available without translations
- for locale in localedata.list():
+ for locale in localedata.locale_identifiers():
if gettext.find('mediagoblin', TRANSLATIONS_PATH, [locale]):
locales.append(locale)
AVAILABLE_LOCALES = locales
diff --git a/mediagoblin/tools/url.py b/mediagoblin/tools/url.py
index d9179f9e..657c0373 100644
--- a/mediagoblin/tools/url.py
+++ b/mediagoblin/tools/url.py
@@ -15,15 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
-# This import *is* used; see word.encode('tranlit/long') below.
-from unicodedata import normalize
-
-try:
- import translitcodec
- USING_TRANSLITCODEC = True
-except ImportError:
- USING_TRANSLITCODEC = False
-
+from unidecode import unidecode
_punct_re = re.compile(r'[\t !"#:$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
@@ -34,11 +26,5 @@ def slugify(text, delim=u'-'):
"""
result = []
for word in _punct_re.split(text.lower()):
- if USING_TRANSLITCODEC:
- word = word.encode('translit/long')
- else:
- word = normalize('NFKD', word).encode('ascii', 'ignore')
-
- if word:
- result.append(word)
+ result.extend(unidecode(word).split())
return unicode(delim.join(result))
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 64fa793e..78751a28 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -31,6 +31,7 @@ from mediagoblin.user_pages.lib import (send_comment_email,
add_media_to_collection, build_report_object)
from mediagoblin.notifications import trigger_notification, \
add_comment_subscription, mark_comment_notification_seen
+from mediagoblin.tools.pluginapi import hook_transform
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
get_media_entry_by_id, user_has_privilege, user_not_banned,
@@ -146,14 +147,23 @@ def media_home(request, media, page, **kwargs):
media_template_name = media.media_manager.display_template
+ context = {
+ 'media': media,
+ 'comments': comments,
+ 'pagination': pagination,
+ 'comment_form': comment_form,
+ 'app_config': mg_globals.app_config}
+
+ # Since the media template name gets swapped out for each media
+ # type, normal context hooks don't work if you want to affect all
+ # media displays. This gives a general purpose hook.
+ context = hook_transform(
+ "media_home_context", context)
+
return render_to_response(
request,
media_template_name,
- {'media': media,
- 'comments': comments,
- 'pagination': pagination,
- 'comment_form': comment_form,
- 'app_config': mg_globals.app_config})
+ context)
@get_media_entry_by_id
diff --git a/mediagoblin/views.py b/mediagoblin/views.py
index 4185c1b6..009e48e4 100644
--- a/mediagoblin/views.py
+++ b/mediagoblin/views.py
@@ -17,13 +17,14 @@
from mediagoblin import mg_globals
from mediagoblin.db.models import MediaEntry
from mediagoblin.tools.pagination import Pagination
+from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.response import render_to_response, render_404
from mediagoblin.decorators import uses_pagination, user_not_banned
@user_not_banned
@uses_pagination
-def root_view(request, page):
+def default_root_view(request, page):
cursor = MediaEntry.query.filter_by(state=u'processed').\
order_by(MediaEntry.created.desc())
@@ -36,6 +37,16 @@ def root_view(request, page):
'pagination': pagination})
+
+def root_view(request):
+ """
+ Proxies to the real root view that's displayed
+ """
+ view = hook_handle("frontpage_view") or default_root_view
+ return view(request)
+
+
+
def simple_template_render(request):
"""
A view for absolutely simple template rendering.
diff --git a/setup.py b/setup.py
index a5b52f18..59f0ab8f 100644
--- a/setup.py
+++ b/setup.py
@@ -53,18 +53,32 @@ try:
'kombu',
'jinja2',
'sphinx',
- 'Babel<1.0',
+ 'Babel>=1.0',
'argparse',
'webtest<2',
'ConfigObj',
'Markdown',
- 'sqlalchemy<0.9.0',
- 'sqlalchemy-migrate',
+ 'sqlalchemy<0.9.0, >0.8.0',
+ # newer sqlalchemy-migrate requires pbr which BREAKS EVERYTHING AND IS
+ # TERRIBLE AND IS THE END OF ALL THINGS
+ # I'd love to remove this restriction.
+ 'sqlalchemy-migrate<0.8',
'mock',
'itsdangerous',
'pytz',
- 'six',
'oauthlib==0.5.0',
+ 'unidecode',
+ 'ExifRead',
+
+ # PLEASE change this when we can; a dependency is forcing us to set this
+ # specific number and it is breaking setup.py develop
+ 'six==1.5.2'
+
+ ## Annoying. Please remove once we can! We only indirectly
+ ## use pbr, and currently it breaks things, presumably till
+ ## their next release.
+ # 'pbr==0.5.22',
+
## This is optional!
# 'translitcodec',
## For now we're expecting that users will install this from