Merge branch 'metadata'
This commit is contained in:
commit
da537ed44e
@ -39,3 +39,70 @@ You can also pass in the `--celery` option if you would prefer that
|
|||||||
your media be passed over to celery to be processed rather than be
|
your media be passed over to celery to be processed rather than be
|
||||||
processed immediately.
|
processed immediately.
|
||||||
|
|
||||||
|
============================
|
||||||
|
Command-line batch uploading
|
||||||
|
============================
|
||||||
|
|
||||||
|
There's another way to submit media, and it can be much more powerful, although
|
||||||
|
it is a bit more complex.
|
||||||
|
|
||||||
|
./bin/gmg batchaddmedia admin /path/to/your/metadata.csv
|
||||||
|
|
||||||
|
This is an example of what a script may look like. The important part here is
|
||||||
|
that you have to create the 'metadata.csv' file.::
|
||||||
|
|
||||||
|
media:location,dcterms:title,dcterms:creator,dcterms:type
|
||||||
|
"http://www.example.net/path/to/nap.png","Goblin taking a nap",,"Image"
|
||||||
|
"http://www.example.net/path/to/snore.ogg","Goblin Snoring","Me","Audio"
|
||||||
|
|
||||||
|
The above is an example of a very simple metadata.csv file. The batchaddmedia
|
||||||
|
script would read this and attempt to upload only two pieces of media, and would
|
||||||
|
be able to automatically name them appropriately.
|
||||||
|
|
||||||
|
The csv file
|
||||||
|
============
|
||||||
|
The location column
|
||||||
|
-------------------
|
||||||
|
The location column is the one column that is absolutely necessary for
|
||||||
|
uploading your media. This gives a path to each piece of media you upload. This
|
||||||
|
can either a path to a local file or a direct link to remote media (with the
|
||||||
|
link in http format). As you can see in the example above the (fake) media was
|
||||||
|
stored remotely on "www.example.net".
|
||||||
|
|
||||||
|
Other internal nodes
|
||||||
|
--------------------
|
||||||
|
There are other columns which can be used by the script to provide information.
|
||||||
|
These are not stored as part of the media's metadata. You can use these columns to
|
||||||
|
provide default information for your media entry, but as you'll see below, it's
|
||||||
|
just as easy to provide this information through the correct metadata columns.
|
||||||
|
|
||||||
|
- **id** is used to identify the media entry to the user in case of an error in the batchaddmedia script.
|
||||||
|
- **license** is used to set a license for your piece a media for mediagoblin's use. This must be a URI.
|
||||||
|
- **title** will set the title displayed to mediagoblin users.
|
||||||
|
- **description** will set a description of your media.
|
||||||
|
|
||||||
|
Metadata columns
|
||||||
|
----------------
|
||||||
|
Other columns can be used to provide detailed metadata about each media entry.
|
||||||
|
Our metadata system accepts any information provided for in the
|
||||||
|
`RDFa Core Initial Context`_, and the batchupload script recognizes all of the
|
||||||
|
resources provided within it.
|
||||||
|
|
||||||
|
.. _RDFa Core Initial Context: http://www.w3.org/2011/rdfa-context/rdfa-1.1
|
||||||
|
|
||||||
|
The uploader may include the metadata for each piece of media, or
|
||||||
|
leave them blank if they want to. A few columns from `Dublin Core`_ are
|
||||||
|
notable because the batchaddmedia script also uses them to set the default
|
||||||
|
information of uploaded media entries.
|
||||||
|
|
||||||
|
.. _Dublin Core: http://wiki.dublincore.org/index.php/User_Guide
|
||||||
|
|
||||||
|
- **dc:title** sets a title for your media entry.
|
||||||
|
- **dc:description** sets a description of your media entry.
|
||||||
|
|
||||||
|
If both a metadata column and an internal node for the title are provided, mediagoblin
|
||||||
|
will use the internal node as the media entry's display name. This makes it so
|
||||||
|
that if you want to display a piece of media with a different title
|
||||||
|
than the one provided in its metadata, you can just provide different data for
|
||||||
|
the 'dc:title' and 'title' columns. The same is true of the 'description' and
|
||||||
|
'dc:description'.
|
||||||
|
@ -31,6 +31,7 @@ from mediagoblin.db.migration_tools import (
|
|||||||
RegisterMigration, inspect_table, replace_table_hack)
|
RegisterMigration, inspect_table, replace_table_hack)
|
||||||
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
||||||
Privilege)
|
Privilege)
|
||||||
|
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||||
|
|
||||||
MIGRATIONS = {}
|
MIGRATIONS = {}
|
||||||
|
|
||||||
@ -720,3 +721,15 @@ def drop_MediaEntry_collected(db):
|
|||||||
media_collected.drop()
|
media_collected.drop()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
@RegisterMigration(20, MIGRATIONS)
|
||||||
|
def add_metadata_column(db):
|
||||||
|
metadata = MetaData(bind=db.bind)
|
||||||
|
|
||||||
|
media_entry = inspect_table(metadata, 'core__media_entries')
|
||||||
|
|
||||||
|
col = Column('media_metadata', MutationDict.as_mutable(JSONEncoded),
|
||||||
|
default=MutationDict())
|
||||||
|
col.create(media_entry)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
@ -264,6 +264,8 @@ class MediaEntry(Base, MediaEntryMixin):
|
|||||||
cascade="all, delete-orphan"
|
cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
collections = association_proxy("collections_helper", "in_collection")
|
collections = association_proxy("collections_helper", "in_collection")
|
||||||
|
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
|
||||||
|
default=MutationDict())
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
# fail_error
|
# fail_error
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import wtforms
|
import wtforms
|
||||||
|
from jsonschema import Draft4Validator
|
||||||
|
|
||||||
from mediagoblin.tools.text import tag_length_validator
|
from mediagoblin.tools.text import tag_length_validator
|
||||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||||
from mediagoblin.tools.licenses import licenses_as_choices
|
from mediagoblin.tools.licenses import licenses_as_choices
|
||||||
|
from mediagoblin.tools.metadata import DEFAULT_SCHEMA, DEFAULT_CHECKER
|
||||||
from mediagoblin.auth.tools import normalize_user_or_email_field
|
from mediagoblin.auth.tools import normalize_user_or_email_field
|
||||||
|
|
||||||
|
|
||||||
@ -122,3 +124,38 @@ class ChangeEmailForm(wtforms.Form):
|
|||||||
[wtforms.validators.Required()],
|
[wtforms.validators.Required()],
|
||||||
description=_(
|
description=_(
|
||||||
"Enter your password to prove you own this account."))
|
"Enter your password to prove you own this account."))
|
||||||
|
|
||||||
|
class MetaDataValidator(object):
|
||||||
|
"""
|
||||||
|
Custom validator which runs form data in a MetaDataForm through a jsonschema
|
||||||
|
validator and passes errors recieved in jsonschema to wtforms.
|
||||||
|
|
||||||
|
:param schema The json schema to validate the data against. By
|
||||||
|
default this uses the DEFAULT_SCHEMA from
|
||||||
|
mediagoblin.tools.metadata.
|
||||||
|
:param format_checker The FormatChecker object that limits which types
|
||||||
|
jsonschema can recognize. By default this uses
|
||||||
|
DEFAULT_CHECKER from mediagoblin.tools.metadata.
|
||||||
|
"""
|
||||||
|
def __init__(self, schema=DEFAULT_SCHEMA, format_checker=DEFAULT_CHECKER):
|
||||||
|
self.schema = schema
|
||||||
|
self.format_checker = format_checker
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
metadata_dict = {field.data:form.value.data}
|
||||||
|
validator = Draft4Validator(self.schema,
|
||||||
|
format_checker=self.format_checker)
|
||||||
|
errors = [e.message
|
||||||
|
for e in validator.iter_errors(metadata_dict)]
|
||||||
|
if len(errors) >= 1:
|
||||||
|
raise wtforms.validators.ValidationError(
|
||||||
|
errors.pop())
|
||||||
|
|
||||||
|
class MetaDataForm(wtforms.Form):
|
||||||
|
identifier = wtforms.TextField(_(u'Identifier'),[MetaDataValidator()])
|
||||||
|
value = wtforms.TextField(_(u'Value'))
|
||||||
|
|
||||||
|
class EditMetaDataForm(wtforms.Form):
|
||||||
|
media_metadata = wtforms.FieldList(
|
||||||
|
wtforms.FormField(MetaDataForm, ""),
|
||||||
|
)
|
||||||
|
@ -17,8 +17,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from itsdangerous import BadSignature
|
from itsdangerous import BadSignature
|
||||||
|
from pyld import jsonld
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
from jsonschema import ValidationError, Draft4Validator
|
||||||
|
|
||||||
from mediagoblin import messages
|
from mediagoblin import messages
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
@ -29,8 +31,11 @@ from mediagoblin.edit import forms
|
|||||||
from mediagoblin.edit.lib import may_edit_media
|
from mediagoblin.edit.lib import may_edit_media
|
||||||
from mediagoblin.decorators import (require_active_login, active_user_from_url,
|
from mediagoblin.decorators import (require_active_login, active_user_from_url,
|
||||||
get_media_entry_by_id, user_may_alter_collection,
|
get_media_entry_by_id, user_may_alter_collection,
|
||||||
get_user_collection)
|
get_user_collection, user_has_privilege,
|
||||||
|
user_not_banned)
|
||||||
from mediagoblin.tools.crypto import get_timed_signer_url
|
from mediagoblin.tools.crypto import get_timed_signer_url
|
||||||
|
from mediagoblin.tools.metadata import (compact_and_validate, DEFAULT_CHECKER,
|
||||||
|
DEFAULT_SCHEMA)
|
||||||
from mediagoblin.tools.mail import email_debug_message
|
from mediagoblin.tools.mail import email_debug_message
|
||||||
from mediagoblin.tools.response import (render_to_response,
|
from mediagoblin.tools.response import (render_to_response,
|
||||||
redirect, redirect_obj, render_404)
|
redirect, redirect_obj, render_404)
|
||||||
@ -432,3 +437,30 @@ def change_email(request):
|
|||||||
'mediagoblin/edit/change_email.html',
|
'mediagoblin/edit/change_email.html',
|
||||||
{'form': form,
|
{'form': form,
|
||||||
'user': user})
|
'user': user})
|
||||||
|
|
||||||
|
@user_has_privilege(u'admin')
|
||||||
|
@require_active_login
|
||||||
|
@get_media_entry_by_id
|
||||||
|
def edit_metadata(request, media):
|
||||||
|
form = forms.EditMetaDataForm(request.form)
|
||||||
|
if request.method == "POST" and form.validate():
|
||||||
|
metadata_dict = dict([(row['identifier'],row['value'])
|
||||||
|
for row in form.media_metadata.data])
|
||||||
|
json_ld_metadata = None
|
||||||
|
json_ld_metadata = compact_and_validate(metadata_dict)
|
||||||
|
media.media_metadata = json_ld_metadata
|
||||||
|
media.save()
|
||||||
|
return redirect_obj(request, media)
|
||||||
|
|
||||||
|
if len(form.media_metadata) == 0:
|
||||||
|
for identifier, value in media.media_metadata.iteritems():
|
||||||
|
if identifier == "@context": continue
|
||||||
|
form.media_metadata.append_entry({
|
||||||
|
'identifier':identifier,
|
||||||
|
'value':value})
|
||||||
|
|
||||||
|
return render_to_response(
|
||||||
|
request,
|
||||||
|
'mediagoblin/edit/metadata.html',
|
||||||
|
{'form':form,
|
||||||
|
'media':media})
|
||||||
|
@ -57,6 +57,10 @@ SUBCOMMAND_MAP = {
|
|||||||
'setup': 'mediagoblin.gmg_commands.deletemedia:parser_setup',
|
'setup': 'mediagoblin.gmg_commands.deletemedia:parser_setup',
|
||||||
'func': 'mediagoblin.gmg_commands.deletemedia:deletemedia',
|
'func': 'mediagoblin.gmg_commands.deletemedia:deletemedia',
|
||||||
'help': 'Delete media entries'},
|
'help': 'Delete media entries'},
|
||||||
|
'batchaddmedia': {
|
||||||
|
'setup': 'mediagoblin.gmg_commands.batchaddmedia:parser_setup',
|
||||||
|
'func': 'mediagoblin.gmg_commands.batchaddmedia:batchaddmedia',
|
||||||
|
'help': 'Add many media entries at once'},
|
||||||
# 'theme': {
|
# 'theme': {
|
||||||
# 'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
|
# 'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
|
||||||
# 'func': 'mediagoblin.gmg_commands.theme:theme',
|
# 'func': 'mediagoblin.gmg_commands.theme:theme',
|
||||||
|
193
mediagoblin/gmg_commands/batchaddmedia.py
Normal file
193
mediagoblin/gmg_commands/batchaddmedia.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from csv import reader as csv_reader
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
|
from mediagoblin.gmg_commands import util as commands_util
|
||||||
|
from mediagoblin.submit.lib import (
|
||||||
|
submit_media, get_upload_file_limits,
|
||||||
|
FileUploadLimit, UserUploadLimit, UserPastUploadLimit)
|
||||||
|
from mediagoblin.tools.metadata import compact_and_validate
|
||||||
|
from mediagoblin.tools.translate import pass_to_ugettext as _
|
||||||
|
from jsonschema.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
def parser_setup(subparser):
|
||||||
|
subparser.description = """\
|
||||||
|
This command allows the administrator to upload many media files at once."""
|
||||||
|
subparser.epilog = _(u"""For more information about how to properly run this
|
||||||
|
script (and how to format the metadata csv file), read the MediaGoblin
|
||||||
|
documentation page on command line uploading
|
||||||
|
<http://docs.mediagoblin.org/siteadmin/commandline-upload.html>""")
|
||||||
|
subparser.add_argument(
|
||||||
|
'username',
|
||||||
|
help=_(u"Name of user these media entries belong to"))
|
||||||
|
subparser.add_argument(
|
||||||
|
'metadata_path',
|
||||||
|
help=_(
|
||||||
|
u"""Path to the csv file containing metadata information."""))
|
||||||
|
subparser.add_argument(
|
||||||
|
'--celery',
|
||||||
|
action='store_true',
|
||||||
|
help=_(u"Don't process eagerly, pass off to celery"))
|
||||||
|
|
||||||
|
|
||||||
|
def batchaddmedia(args):
|
||||||
|
# Run eagerly unless explicetly set not to
|
||||||
|
if not args.celery:
|
||||||
|
os.environ['CELERY_ALWAYS_EAGER'] = 'true'
|
||||||
|
|
||||||
|
app = commands_util.setup_app(args)
|
||||||
|
|
||||||
|
files_uploaded, files_attempted = 0, 0
|
||||||
|
|
||||||
|
# get the user
|
||||||
|
user = app.db.User.query.filter_by(username=args.username.lower()).first()
|
||||||
|
if user is None:
|
||||||
|
print _(u"Sorry, no user by username '{username}' exists".format(
|
||||||
|
username=args.username))
|
||||||
|
return
|
||||||
|
|
||||||
|
upload_limit, max_file_size = get_upload_file_limits(user)
|
||||||
|
temp_files = []
|
||||||
|
|
||||||
|
if os.path.isfile(args.metadata_path):
|
||||||
|
metadata_path = args.metadata_path
|
||||||
|
|
||||||
|
else:
|
||||||
|
error = _(u'File at {path} not found, use -h flag for help'.format(
|
||||||
|
path=args.metadata_path))
|
||||||
|
print error
|
||||||
|
return
|
||||||
|
|
||||||
|
abs_metadata_filename = os.path.abspath(metadata_path)
|
||||||
|
abs_metadata_dir = os.path.dirname(abs_metadata_filename)
|
||||||
|
upload_limit, max_file_size = get_upload_file_limits(user)
|
||||||
|
|
||||||
|
def maybe_unicodeify(some_string):
|
||||||
|
# this is kinda terrible
|
||||||
|
if some_string is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return unicode(some_string)
|
||||||
|
|
||||||
|
with file(abs_metadata_filename, 'r') as all_metadata:
|
||||||
|
contents = all_metadata.read()
|
||||||
|
media_metadata = parse_csv_file(contents)
|
||||||
|
|
||||||
|
for media_id, file_metadata in media_metadata.iteritems():
|
||||||
|
files_attempted += 1
|
||||||
|
# In case the metadata was not uploaded initialize an empty dictionary.
|
||||||
|
json_ld_metadata = compact_and_validate({})
|
||||||
|
|
||||||
|
# Get all metadata entries starting with 'media' as variables and then
|
||||||
|
# delete them because those are for internal use only.
|
||||||
|
original_location = file_metadata['location']
|
||||||
|
|
||||||
|
### Pull the important media information for mediagoblin from the
|
||||||
|
### metadata, if it is provided.
|
||||||
|
title = file_metadata.get('title') or file_metadata.get('dc:title')
|
||||||
|
description = (file_metadata.get('description') or
|
||||||
|
file_metadata.get('dc:description'))
|
||||||
|
|
||||||
|
license = file_metadata.get('license')
|
||||||
|
try:
|
||||||
|
json_ld_metadata = compact_and_validate(file_metadata)
|
||||||
|
except ValidationError, exc:
|
||||||
|
error = _(u"""Error with media '{media_id}' value '{error_path}': {error_msg}
|
||||||
|
Metadata was not uploaded.""".format(
|
||||||
|
media_id=media_id,
|
||||||
|
error_path=exc.path[0],
|
||||||
|
error_msg=exc.message))
|
||||||
|
print error
|
||||||
|
continue
|
||||||
|
|
||||||
|
url = urlparse(original_location)
|
||||||
|
filename = url.path.split()[-1]
|
||||||
|
|
||||||
|
if url.scheme == 'http':
|
||||||
|
res = requests.get(url.geturl(), stream=True)
|
||||||
|
media_file = res.raw
|
||||||
|
|
||||||
|
elif url.scheme == '':
|
||||||
|
path = url.path
|
||||||
|
if os.path.isabs(path):
|
||||||
|
file_abs_path = os.path.abspath(path)
|
||||||
|
else:
|
||||||
|
file_path = os.path.join(abs_metadata_dir, path)
|
||||||
|
file_abs_path = os.path.abspath(file_path)
|
||||||
|
try:
|
||||||
|
media_file = file(file_abs_path, 'r')
|
||||||
|
except IOError:
|
||||||
|
print _(u"""\
|
||||||
|
FAIL: Local file {filename} could not be accessed.
|
||||||
|
{filename} will not be uploaded.""".format(filename=filename))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
submit_media(
|
||||||
|
mg_app=app,
|
||||||
|
user=user,
|
||||||
|
submitted_file=media_file,
|
||||||
|
filename=filename,
|
||||||
|
title=maybe_unicodeify(title),
|
||||||
|
description=maybe_unicodeify(description),
|
||||||
|
license=maybe_unicodeify(license),
|
||||||
|
metadata=json_ld_metadata,
|
||||||
|
tags_string=u"",
|
||||||
|
upload_limit=upload_limit, max_file_size=max_file_size)
|
||||||
|
print _(u"""Successfully submitted {filename}!
|
||||||
|
Be sure to look at the Media Processing Panel on your website to be sure it
|
||||||
|
uploaded successfully.""".format(filename=filename))
|
||||||
|
files_uploaded += 1
|
||||||
|
except FileUploadLimit:
|
||||||
|
print _(
|
||||||
|
u"FAIL: This file is larger than the upload limits for this site.")
|
||||||
|
except UserUploadLimit:
|
||||||
|
print _(
|
||||||
|
"FAIL: This file will put this user past their upload limits.")
|
||||||
|
except UserPastUploadLimit:
|
||||||
|
print _("FAIL: This user is already past their upload limits.")
|
||||||
|
print _(
|
||||||
|
"{files_uploaded} out of {files_attempted} files successfully submitted".format(
|
||||||
|
files_uploaded=files_uploaded,
|
||||||
|
files_attempted=files_attempted))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_csv_file(file_contents):
|
||||||
|
"""
|
||||||
|
The helper function which converts the csv file into a dictionary where each
|
||||||
|
item's key is the provided value 'id' and each item's value is another
|
||||||
|
dictionary.
|
||||||
|
"""
|
||||||
|
list_of_contents = file_contents.split('\n')
|
||||||
|
key, lines = (list_of_contents[0].split(','),
|
||||||
|
list_of_contents[1:])
|
||||||
|
objects_dict = {}
|
||||||
|
|
||||||
|
# Build a dictionary
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if line.isspace() or line == '': continue
|
||||||
|
values = csv_reader([line]).next()
|
||||||
|
line_dict = dict([(key[i], val)
|
||||||
|
for i, val in enumerate(values)])
|
||||||
|
media_id = line_dict.get('id') or index
|
||||||
|
objects_dict[media_id] = (line_dict)
|
||||||
|
|
||||||
|
return objects_dict
|
||||||
|
|
41
mediagoblin/plugins/metadata_display/__init__.py
Normal file
41
mediagoblin/plugins/metadata_display/__init__.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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/>.
|
||||||
|
import os
|
||||||
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
|
from mediagoblin.plugins.metadata_display.lib import add_rdfa_to_readable_to_media_home
|
||||||
|
from mediagoblin.tools import pluginapi
|
||||||
|
from mediagoblin.tools.staticdirect import PluginStatic
|
||||||
|
|
||||||
|
PLUGIN_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
def setup_plugin():
|
||||||
|
# Register the template path.
|
||||||
|
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
|
||||||
|
|
||||||
|
pluginapi.register_template_hooks(
|
||||||
|
{"media_sideinfo": "mediagoblin/plugins/metadata_display/metadata_table.html",
|
||||||
|
"head": "mediagoblin/plugins/metadata_display/bits/metadata_extra_head.html"})
|
||||||
|
|
||||||
|
|
||||||
|
hooks = {
|
||||||
|
'setup': setup_plugin,
|
||||||
|
'static_setup': lambda: PluginStatic(
|
||||||
|
'metadata_display',
|
||||||
|
resource_filename('mediagoblin.plugins.metadata_display', 'static')
|
||||||
|
),
|
||||||
|
'media_home_context':add_rdfa_to_readable_to_media_home
|
||||||
|
}
|
31
mediagoblin/plugins/metadata_display/lib.py
Normal file
31
mediagoblin/plugins/metadata_display/lib.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
def rdfa_to_readable(rdfa_predicate):
|
||||||
|
"""
|
||||||
|
A simple script to convert rdfa resource descriptors into a form more
|
||||||
|
accessible for humans.
|
||||||
|
"""
|
||||||
|
readable = rdfa_predicate.split(u":")[1].capitalize()
|
||||||
|
return readable
|
||||||
|
|
||||||
|
def add_rdfa_to_readable_to_media_home(context):
|
||||||
|
"""
|
||||||
|
A context hook which adds the 'rdfa_to_readable' filter to
|
||||||
|
the media home page.
|
||||||
|
"""
|
||||||
|
context['rdfa_to_readable'] = rdfa_to_readable
|
||||||
|
return context
|
@ -0,0 +1,14 @@
|
|||||||
|
table.metadata_info {
|
||||||
|
font-size:85%;
|
||||||
|
margin-left:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.metadata_info th {
|
||||||
|
font-weight: bold;
|
||||||
|
border-spacing: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
table.metadata_info td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
<link rel="stylesheet" type="text/css"
|
||||||
|
href="{{ request.staticdirect('css/metadata_display.css',
|
||||||
|
'metadata_display') }}"/>
|
@ -0,0 +1,42 @@
|
|||||||
|
{#
|
||||||
|
# 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/>.
|
||||||
|
#}
|
||||||
|
{% block metadata_information_table %}
|
||||||
|
{%- set metadata=media.media_metadata %}
|
||||||
|
{%- set metadata_context=metadata['@context'] %}
|
||||||
|
{%- if metadata %}
|
||||||
|
<h3>{% trans %}Metadata{% endtrans %}</h3>
|
||||||
|
{#- NOTE: In some smart future where the context is more extensible,
|
||||||
|
we will need to add to the prefix here-#}
|
||||||
|
<table class="metadata_info">
|
||||||
|
{%- for key, value in metadata.iteritems() if not key=='@context' %}
|
||||||
|
{% if value -%}
|
||||||
|
<tr>
|
||||||
|
<th>{{ rdfa_to_readable(key) }}</th>
|
||||||
|
<td property="{{ key }}">{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user and request.user.has_privilege('admin') %}
|
||||||
|
<a href="{{ request.urlgen('mediagoblin.edit.metadata',
|
||||||
|
user=media.get_uploader.username,
|
||||||
|
media_id=media.id) }}">
|
||||||
|
{% trans %}Edit Metadata{% endtrans %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -0,0 +1,60 @@
|
|||||||
|
{#
|
||||||
|
# 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/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<<<<<<< HEAD:mediagoblin/templates/mediagoblin/utils/metadata_table.html
|
||||||
|
{%- macro render_table(request, media_entry, format_predicate) %}
|
||||||
|
{%- set metadata=media_entry.media_metadata %}
|
||||||
|
{%- set metadata_context=metadata['@context'] %}
|
||||||
|
{%- if metadata %}
|
||||||
|
<h3>{% trans %}Metadata Information{% endtrans %}</h3>
|
||||||
|
<table class="metadata_info">
|
||||||
|
{%- for key, value in metadata.iteritems() if (
|
||||||
|
not key=='@context' and value) %}
|
||||||
|
<tr {% if loop.index%2 == 1 %}class="highlight"{% endif %}>
|
||||||
|
<th>{{ format_predicate(key) }}</th>
|
||||||
|
<td property="{{ key }}">
|
||||||
|
{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user and request.user.has_privilege('admin') %}
|
||||||
|
<a href="{{ request.urlgen('mediagoblin.edit.metadata',
|
||||||
|
user=media_entry.get_uploader.username,
|
||||||
|
media_id=media_entry.id) }}">
|
||||||
|
{% trans %}Edit Metadata{% endtrans %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
=======
|
||||||
|
{%- set metadata=media.media_metadata %}
|
||||||
|
{%- set metadata_context=metadata['@context'] %}
|
||||||
|
{%- if metadata %}
|
||||||
|
{#- NOTE: In some smart future where the context is more extensible,
|
||||||
|
we will need to add to the prefix here-#}
|
||||||
|
<table>
|
||||||
|
{%- for key, value in metadata.iteritems() if not key=='@context' %}
|
||||||
|
{% if value -%}
|
||||||
|
<tr>
|
||||||
|
<td>{{ rdfa_to_readable(key) }}</td>
|
||||||
|
<td property="{{ key }}">{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
>>>>>>> acfcaf6366bd4695c1c37c7aa8ff5a176b412e2a:mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html
|
@ -28,7 +28,7 @@ _log = logging.getLogger(__name__)
|
|||||||
def get_url_map():
|
def get_url_map():
|
||||||
add_route('index', '/', 'mediagoblin.views:root_view')
|
add_route('index', '/', 'mediagoblin.views:root_view')
|
||||||
add_route('terms_of_service','/terms_of_service',
|
add_route('terms_of_service','/terms_of_service',
|
||||||
'mediagoblin.views:terms_of_service')
|
'mediagoblin.views:terms_of_service'),
|
||||||
mount('/auth', auth_routes)
|
mount('/auth', auth_routes)
|
||||||
mount('/mod', moderation_routes)
|
mount('/mod', moderation_routes)
|
||||||
|
|
||||||
|
@ -609,7 +609,6 @@ a img.media_image {
|
|||||||
cursor: -moz-zoom-in;
|
cursor: -moz-zoom-in;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* icons */
|
/* icons */
|
||||||
|
|
||||||
img.media_icon {
|
img.media_icon {
|
||||||
@ -938,3 +937,16 @@ p.verifier {
|
|||||||
none repeat scroll 0% 0% rgb(221, 221, 221);
|
none repeat scroll 0% 0% rgb(221, 221, 221);
|
||||||
padding: 1em 0px;
|
padding: 1em 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* for the media metadata editing table */
|
||||||
|
table.metadata_editor {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.metadata_editor tr td {
|
||||||
|
width:350px;
|
||||||
|
}
|
||||||
|
table.metadata_editor tr td.form_field_input input {
|
||||||
|
width:350px;
|
||||||
|
}
|
||||||
|
48
mediagoblin/static/metadata/rdfa11.jsonld
Normal file
48
mediagoblin/static/metadata/rdfa11.jsonld
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"@context": {
|
||||||
|
"cat": "http://www.w3.org/ns/dcat#",
|
||||||
|
"qb": "http://purl.org/linked-data/cube#",
|
||||||
|
"grddl": "http://www.w3.org/2003/g/data-view#",
|
||||||
|
"ma": "http://www.w3.org/ns/ma-ont#",
|
||||||
|
"owl": "http://www.w3.org/2002/07/owl#",
|
||||||
|
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||||
|
"rdfa": "http://www.w3.org/ns/rdfa#",
|
||||||
|
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||||
|
"rif": "http://www.w3.org/2007/rif#",
|
||||||
|
"rr": "http://www.w3.org/ns/r2rml#",
|
||||||
|
"skos": "http://www.w3.org/2004/02/skos/core#",
|
||||||
|
"skosxl": "http://www.w3.org/2008/05/skos-xl#",
|
||||||
|
"wdr": "http://www.w3.org/2007/05/powder#",
|
||||||
|
"void": "http://rdfs.org/ns/void#",
|
||||||
|
"wdrs": "http://www.w3.org/2007/05/powder-s#",
|
||||||
|
"xhv": "http://www.w3.org/1999/xhtml/vocab#",
|
||||||
|
"xml": "http://www.w3.org/XML/1998/namespace",
|
||||||
|
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||||
|
"prov": "http://www.w3.org/ns/prov#",
|
||||||
|
"sd": "http://www.w3.org/ns/sparql-service-description#",
|
||||||
|
"org": "http://www.w3.org/ns/org#",
|
||||||
|
"gldp": "http://www.w3.org/ns/people#",
|
||||||
|
"cnt": "http://www.w3.org/2008/content#",
|
||||||
|
"dcat": "http://www.w3.org/ns/dcat#",
|
||||||
|
"earl": "http://www.w3.org/ns/earl#",
|
||||||
|
"ht": "http://www.w3.org/2006/http#",
|
||||||
|
"ptr": "http://www.w3.org/2009/pointers#",
|
||||||
|
"cc": "http://creativecommons.org/ns#",
|
||||||
|
"ctag": "http://commontag.org/ns#",
|
||||||
|
"dc": "http://purl.org/dc/terms/",
|
||||||
|
"dc11": "http://purl.org/dc/elements/1.1/",
|
||||||
|
"dcterms": "http://purl.org/dc/terms/",
|
||||||
|
"foaf": "http://xmlns.com/foaf/0.1/",
|
||||||
|
"gr": "http://purl.org/goodrelations/v1#",
|
||||||
|
"ical": "http://www.w3.org/2002/12/cal/icaltzd#",
|
||||||
|
"og": "http://ogp.me/ns#",
|
||||||
|
"rev": "http://purl.org/stuff/rev#",
|
||||||
|
"sioc": "http://rdfs.org/sioc/ns#",
|
||||||
|
"v": "http://rdf.data-vocabulary.org/#",
|
||||||
|
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||||
|
"schema": "http://schema.org/",
|
||||||
|
"describedby": "http://www.w3.org/2007/05/powder-s#describedby",
|
||||||
|
"license": "http://www.w3.org/1999/xhtml/vocab#license",
|
||||||
|
"role": "http://www.w3.org/1999/xhtml/vocab#role"
|
||||||
|
}
|
||||||
|
}
|
@ -98,7 +98,7 @@ class UserPastUploadLimit(UploadLimitError):
|
|||||||
|
|
||||||
def submit_media(mg_app, user, submitted_file, filename,
|
def submit_media(mg_app, user, submitted_file, filename,
|
||||||
title=None, description=None,
|
title=None, description=None,
|
||||||
license=None, tags_string=u"",
|
license=None, metadata=None, tags_string=u"",
|
||||||
upload_limit=None, max_file_size=None,
|
upload_limit=None, max_file_size=None,
|
||||||
callback_url=None,
|
callback_url=None,
|
||||||
# If provided we'll do the feed_url update, otherwise ignore
|
# If provided we'll do the feed_url update, otherwise ignore
|
||||||
@ -142,6 +142,8 @@ def submit_media(mg_app, user, submitted_file, filename,
|
|||||||
|
|
||||||
entry.license = license or None
|
entry.license = license or None
|
||||||
|
|
||||||
|
entry.media_metadata = metadata or {}
|
||||||
|
|
||||||
# Process the user's folksonomy "tags"
|
# Process the user's folksonomy "tags"
|
||||||
entry.tags = convert_to_tag_list_of_dicts(tags_string)
|
entry.tags = convert_to_tag_list_of_dicts(tags_string)
|
||||||
|
|
||||||
|
94
mediagoblin/templates/mediagoblin/edit/metadata.html
Normal file
94
mediagoblin/templates/mediagoblin/edit/metadata.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{#
|
||||||
|
# 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/>.
|
||||||
|
#}
|
||||||
|
{%- extends "mediagoblin/base.html" %}
|
||||||
|
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
|
||||||
|
{% block mediagoblin_head %}
|
||||||
|
<script>
|
||||||
|
function add_new_row(table_id, row_number, input_prefix) {
|
||||||
|
new_row = $('<tr>').append(
|
||||||
|
$('<td>').attr(
|
||||||
|
'class','form_field_input').append(
|
||||||
|
$('<input>').attr('name',
|
||||||
|
input_prefix + row_number + "-identifier").attr('id',
|
||||||
|
input_prefix + row_number + "-identifier")
|
||||||
|
)
|
||||||
|
).append(
|
||||||
|
$('<td>').attr(
|
||||||
|
'class','form_field_input').append(
|
||||||
|
$('<input>').attr('name',
|
||||||
|
input_prefix + row_number + "-value").attr('id',
|
||||||
|
input_prefix + row_number + "-value")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$(table_id).append(new_row);
|
||||||
|
}
|
||||||
|
function clear_empty_rows(list_id) {
|
||||||
|
$('table'+list_id+' tr').each(function(row){
|
||||||
|
id_input = $(this).find('td').find('input');
|
||||||
|
value_input = $(this).find('td').next().find('input');
|
||||||
|
if ((value_input.attr('value') == "") &&
|
||||||
|
(id_input.attr('value') == "")) {
|
||||||
|
$(this).remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function(){
|
||||||
|
var metadata_lines = {{ form.media_metadata | length }};
|
||||||
|
$("#add_new_metadata_row").click(function(){
|
||||||
|
add_new_row("#metadata_list",
|
||||||
|
metadata_lines,
|
||||||
|
'media_metadata-');
|
||||||
|
metadata_lines += 1;
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#clear_empty_rows").click(function(){
|
||||||
|
clear_empty_rows("#metadata_list");
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block mediagoblin_content %}
|
||||||
|
<h2>{% trans media_name=media.title -%}
|
||||||
|
Metadata for "{{ media_name }}"{% endtrans %}</h2>
|
||||||
|
<form action="" method="POST" id="metadata_form">
|
||||||
|
|
||||||
|
<!-- This table holds all the information about the media entry's metadata -->
|
||||||
|
<h3>{% trans %}MetaData{% endtrans %}</h3>
|
||||||
|
<table class="metadata_editor" id="metadata_list" >
|
||||||
|
{{ wtforms_util.render_fieldlist_as_table_rows(form.media_metadata) }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- These are the buttons you use to control the form -->
|
||||||
|
<table class="metadata_editor" id="buttons_bottom">
|
||||||
|
<tr>
|
||||||
|
<td><input type=button value="{% trans %}Add new Row{% endtrans %}"
|
||||||
|
class="button_action" id="add_new_metadata_row" />
|
||||||
|
</td>
|
||||||
|
<td><input type=submit value="{% trans %}Update Metadata{% endtrans %}"
|
||||||
|
class="button_action_highlight" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type=button value="{% trans %}Clear empty Rows{% endtrans %}"
|
||||||
|
class="button_action" id="clear_empty_rows" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{ csrf_token }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock mediagoblin_content %}
|
@ -230,9 +230,6 @@
|
|||||||
|
|
||||||
{% template_hook("media_sideinfo") %}
|
{% template_hook("media_sideinfo") %}
|
||||||
|
|
||||||
{% block mediagoblin_sidebar %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</div><!--end media_sidebar-->
|
</div><!--end media_sidebar-->
|
||||||
|
|
||||||
<div class="clear"></div>
|
<div class="clear"></div>
|
||||||
|
@ -70,23 +70,56 @@
|
|||||||
{# Auto-render a form as a table #}
|
{# Auto-render a form as a table #}
|
||||||
{% macro render_table(form) -%}
|
{% macro render_table(form) -%}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<tr>
|
render_field_as_table_row(field)
|
||||||
<th>{{ field.label.text }}</th>
|
|
||||||
<td>
|
|
||||||
{{field}}
|
|
||||||
{% if field.errors %}
|
|
||||||
<br />
|
|
||||||
<ul class="errors">
|
|
||||||
{% for error in field.errors %}
|
|
||||||
<li>{{error}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_form_as_table_row(form) %}
|
||||||
|
<tr>
|
||||||
|
{%- for field in form %}
|
||||||
|
<td class="form_field_input">
|
||||||
|
{{field}}
|
||||||
|
</td>
|
||||||
|
{%- endfor %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{%- for field in form %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="form_field_error">{{error}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_field_as_table_row(field) %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ field.label.text }}</th>
|
||||||
|
<td>
|
||||||
|
{{field}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="form_field_error">{{error}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_fieldlist_as_table_rows(fieldlist) %}
|
||||||
|
{% for field in fieldlist -%}
|
||||||
|
{%- if field.type == 'FormField' %}
|
||||||
|
{{ render_form_as_table_row(field) }}
|
||||||
|
{%- else %}
|
||||||
|
{{ render_field_as_table_row(field) }}
|
||||||
|
{%- endif %}
|
||||||
|
{% endfor -%}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{# Render a boolean field #}
|
{# Render a boolean field #}
|
||||||
{% macro render_bool(field) %}
|
{% macro render_bool(field) %}
|
||||||
<div class="boolean">
|
<div class="boolean">
|
||||||
|
@ -14,11 +14,11 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import urlparse
|
import urlparse, os, pytest
|
||||||
|
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
from mediagoblin.db.models import User
|
from mediagoblin.db.models import User, MediaEntry
|
||||||
from mediagoblin.tests.tools import fixture_add_user
|
from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry
|
||||||
from mediagoblin import auth
|
from mediagoblin import auth
|
||||||
from mediagoblin.tools import template, mail
|
from mediagoblin.tools import template, mail
|
||||||
|
|
||||||
@ -174,3 +174,81 @@ class TestUserEdit(object):
|
|||||||
email = User.query.filter_by(username='chris').first().email
|
email = User.query.filter_by(username='chris').first().email
|
||||||
assert email == 'new@example.com'
|
assert email == 'new@example.com'
|
||||||
# test changing the url inproperly
|
# test changing the url inproperly
|
||||||
|
|
||||||
|
class TestMetaDataEdit:
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(self, test_app):
|
||||||
|
# set up new user
|
||||||
|
self.user_password = u'toast'
|
||||||
|
self.user = fixture_add_user(password = self.user_password,
|
||||||
|
privileges=[u'active',u'admin'])
|
||||||
|
self.test_app = test_app
|
||||||
|
|
||||||
|
def login(self, test_app):
|
||||||
|
test_app.post(
|
||||||
|
'/auth/login/', {
|
||||||
|
'username': self.user.username,
|
||||||
|
'password': self.user_password})
|
||||||
|
|
||||||
|
def do_post(self, data, *context_keys, **kwargs):
|
||||||
|
url = kwargs.pop('url', '/submit/')
|
||||||
|
do_follow = kwargs.pop('do_follow', False)
|
||||||
|
template.clear_test_template_context()
|
||||||
|
response = self.test_app.post(url, data, **kwargs)
|
||||||
|
if do_follow:
|
||||||
|
response.follow()
|
||||||
|
context_data = template.TEMPLATE_TEST_CONTEXT
|
||||||
|
for key in context_keys:
|
||||||
|
context_data = context_data[key]
|
||||||
|
return response, context_data
|
||||||
|
|
||||||
|
def test_edit_metadata(self, test_app):
|
||||||
|
media_entry = fixture_media_entry(uploader=self.user.id,
|
||||||
|
state=u'processed')
|
||||||
|
media_slug = "/u/{username}/m/{media_id}/metadata/".format(
|
||||||
|
username = str(self.user.username),
|
||||||
|
media_id = str(media_entry.id))
|
||||||
|
|
||||||
|
self.login(test_app)
|
||||||
|
response = test_app.get(media_slug)
|
||||||
|
assert response.status == '200 OK'
|
||||||
|
assert media_entry.media_metadata == {}
|
||||||
|
# First test adding in metadata
|
||||||
|
################################
|
||||||
|
response, context = self.do_post({
|
||||||
|
"media_metadata-0-identifier":"dc:title",
|
||||||
|
"media_metadata-0-value":"Some title",
|
||||||
|
"media_metadata-1-identifier":"dc:creator",
|
||||||
|
"media_metadata-1-value":"Me"},url=media_slug)
|
||||||
|
|
||||||
|
media_entry = MediaEntry.query.first()
|
||||||
|
new_metadata = media_entry.media_metadata
|
||||||
|
assert new_metadata != {}
|
||||||
|
assert new_metadata.get("dc:title") == "Some title"
|
||||||
|
assert new_metadata.get("dc:creator") == "Me"
|
||||||
|
# Now test removing the metadata
|
||||||
|
################################
|
||||||
|
response, context = self.do_post({
|
||||||
|
"media_metadata-0-identifier":"dc:title",
|
||||||
|
"media_metadata-0-value":"Some title"},url=media_slug)
|
||||||
|
|
||||||
|
media_entry = MediaEntry.query.first()
|
||||||
|
new_metadata = media_entry.media_metadata
|
||||||
|
assert new_metadata.get("dc:title") == "Some title"
|
||||||
|
assert new_metadata.get("dc:creator") is None
|
||||||
|
# Now test adding bad metadata
|
||||||
|
###############################
|
||||||
|
response, context = self.do_post({
|
||||||
|
"media_metadata-0-identifier":"dc:title",
|
||||||
|
"media_metadata-0-value":"Some title",
|
||||||
|
"media_metadata-1-identifier":"dc:creator",
|
||||||
|
"media_metadata-1-value":"Me",
|
||||||
|
"media_metadata-2-identifier":"dc:created",
|
||||||
|
"media_metadata-2-value":"On the worst day"},url=media_slug)
|
||||||
|
|
||||||
|
media_entry = MediaEntry.query.first()
|
||||||
|
old_metadata = new_metadata
|
||||||
|
new_metadata = media_entry.media_metadata
|
||||||
|
assert new_metadata == old_metadata
|
||||||
|
assert ("u'On the worst day' is not a 'date-time'" in
|
||||||
|
response.body)
|
||||||
|
78
mediagoblin/tests/test_metadata.py
Normal file
78
mediagoblin/tests/test_metadata.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mediagoblin.tools.metadata import compact_and_validate
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
class TestMetadataFunctionality:
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _setup(self, test_app):
|
||||||
|
self.test_app = test_app
|
||||||
|
|
||||||
|
def testCompactAndValidate(self):
|
||||||
|
# First, test out a well formatted piece of metadata
|
||||||
|
######################################################
|
||||||
|
test_metadata = {
|
||||||
|
'dc:title':'My Pet Bunny',
|
||||||
|
'dc:description':'A picture displaying how cute my pet bunny is.',
|
||||||
|
'location':'/home/goblin/Pictures/bunny.png',
|
||||||
|
'license':'http://www.gnu.org/licenses/gpl.txt'
|
||||||
|
}
|
||||||
|
jsonld_metadata =compact_and_validate(test_metadata)
|
||||||
|
assert jsonld_metadata
|
||||||
|
assert jsonld_metadata.get('dc:title') == 'My Pet Bunny'
|
||||||
|
# Free floating nodes should be removed
|
||||||
|
assert jsonld_metadata.get('location') is None
|
||||||
|
assert jsonld_metadata.get('@context') == \
|
||||||
|
u"http://www.w3.org/2013/json-ld-context/rdfa11"
|
||||||
|
|
||||||
|
# Next, make sure that various badly formatted metadata
|
||||||
|
# will be rejected.
|
||||||
|
#######################################################
|
||||||
|
#,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
|
||||||
|
# Metadata with a non-URI license should fail :
|
||||||
|
#`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'
|
||||||
|
metadata_fail_1 = {
|
||||||
|
'dc:title':'My Pet Bunny',
|
||||||
|
'dc:description':'A picture displaying how cute my pet bunny is.',
|
||||||
|
'location':'/home/goblin/Pictures/bunny.png',
|
||||||
|
'license':'All Rights Reserved.'
|
||||||
|
}
|
||||||
|
jsonld_fail_1 = None
|
||||||
|
try:
|
||||||
|
jsonld_fail_1 = compact_and_validate(metadata_fail_1)
|
||||||
|
except ValidationError, e:
|
||||||
|
assert e.message == "'All Rights Reserved.' is not a 'uri'"
|
||||||
|
assert jsonld_fail_1 == None
|
||||||
|
#,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,
|
||||||
|
# Metadata with an ivalid date-time dc:created should fail :
|
||||||
|
#`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`'`''
|
||||||
|
metadata_fail_2 = {
|
||||||
|
'dc:title':'My Pet Bunny',
|
||||||
|
'dc:description':'A picture displaying how cute my pet bunny is.',
|
||||||
|
'location':'/home/goblin/Pictures/bunny.png',
|
||||||
|
'license':'http://www.gnu.org/licenses/gpl.txt',
|
||||||
|
'dc:created':'The other day'
|
||||||
|
}
|
||||||
|
jsonld_fail_2 = None
|
||||||
|
try:
|
||||||
|
jsonld_fail_2 = compact_and_validate(metadata_fail_2)
|
||||||
|
except ValidationError, e:
|
||||||
|
assert e.message == "'The other day' is not a 'date-time'"
|
||||||
|
assert jsonld_fail_2 == None
|
||||||
|
|
222
mediagoblin/tools/metadata.py
Normal file
222
mediagoblin/tools/metadata.py
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
from pyld import jsonld
|
||||||
|
from jsonschema import validate, FormatChecker, draft4_format_checker
|
||||||
|
from jsonschema.compat import str_types
|
||||||
|
|
||||||
|
from mediagoblin.tools.pluginapi import hook_handle
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
## Set up the MediaGoblin format checker for json-schema
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
URL_REGEX = re.compile(
|
||||||
|
r'^[a-z]+://([^/:]+|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]+)?(\/.*)?$',
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
def is_uri(instance):
|
||||||
|
"""
|
||||||
|
jsonschema uri validator
|
||||||
|
"""
|
||||||
|
if not isinstance(instance, str_types):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return URL_REGEX.match(instance)
|
||||||
|
|
||||||
|
def is_datetime(instance):
|
||||||
|
"""
|
||||||
|
Is a date or datetime readable string.
|
||||||
|
"""
|
||||||
|
if not isinstance(instance, str_types):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return dateutil.parser.parse(instance)
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultChecker(FormatChecker):
|
||||||
|
"""
|
||||||
|
Default MediaGoblin format checker... extended to include a few extra things
|
||||||
|
"""
|
||||||
|
checkers = copy.deepcopy(draft4_format_checker.checkers)
|
||||||
|
|
||||||
|
|
||||||
|
DefaultChecker.checkers[u"uri"] = (is_uri, ())
|
||||||
|
DefaultChecker.checkers[u"date-time"] = (is_datetime, (ValueError, TypeError))
|
||||||
|
DEFAULT_CHECKER = DefaultChecker()
|
||||||
|
|
||||||
|
# Crappy default schema, checks for things we deem important
|
||||||
|
|
||||||
|
DEFAULT_SCHEMA = {
|
||||||
|
"$schema": "http://json-schema.org/schema#",
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"license": {
|
||||||
|
"format": "uri",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"dcterms:created": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"dc:created": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_resource(package, resource_path):
|
||||||
|
"""
|
||||||
|
Load a resource, return it as a string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- package: package or module name. Eg "mediagoblin.media_types.audio"
|
||||||
|
- resource_path: path to get to this resource, a list of
|
||||||
|
directories and finally a filename. Will be joined with
|
||||||
|
os.path.sep.
|
||||||
|
"""
|
||||||
|
filename = resource_filename(package, os.path.sep.join(resource_path))
|
||||||
|
return file(filename).read()
|
||||||
|
|
||||||
|
def load_resource_json(package, resource_path):
|
||||||
|
"""
|
||||||
|
Load a resource json file, return a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- package: package or module name. Eg "mediagoblin.media_types.audio"
|
||||||
|
- resource_path: path to get to this resource, a list of
|
||||||
|
directories and finally a filename. Will be joined with
|
||||||
|
os.path.sep.
|
||||||
|
"""
|
||||||
|
return json.loads(load_resource(package, resource_path))
|
||||||
|
|
||||||
|
|
||||||
|
##################################
|
||||||
|
## Load the MediaGoblin core files
|
||||||
|
##################################
|
||||||
|
|
||||||
|
|
||||||
|
BUILTIN_CONTEXTS = {
|
||||||
|
"http://www.w3.org/2013/json-ld-context/rdfa11": load_resource(
|
||||||
|
"mediagoblin", ["static", "metadata", "rdfa11.jsonld"])}
|
||||||
|
|
||||||
|
|
||||||
|
_CONTEXT_CACHE = {}
|
||||||
|
|
||||||
|
def load_context(url):
|
||||||
|
"""
|
||||||
|
A self-aware document loader. For those contexts MediaGoblin
|
||||||
|
stores internally, load them from disk.
|
||||||
|
"""
|
||||||
|
if url in _CONTEXT_CACHE:
|
||||||
|
return _CONTEXT_CACHE[url]
|
||||||
|
|
||||||
|
# See if it's one of our basic ones
|
||||||
|
document = BUILTIN_CONTEXTS.get(url, None)
|
||||||
|
|
||||||
|
# No? See if we have an internal schema for this
|
||||||
|
if document is None:
|
||||||
|
document = hook_handle(("context_url_data", url))
|
||||||
|
|
||||||
|
# Okay, if we've gotten a document by now... let's package it up
|
||||||
|
if document is not None:
|
||||||
|
document = {'contextUrl': None,
|
||||||
|
'documentUrl': url,
|
||||||
|
'document': document}
|
||||||
|
|
||||||
|
# Otherwise, use jsonld.load_document
|
||||||
|
else:
|
||||||
|
document = jsonld.load_document(url)
|
||||||
|
|
||||||
|
# cache
|
||||||
|
_CONTEXT_CACHE[url] = document
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONTEXT = "http://www.w3.org/2013/json-ld-context/rdfa11"
|
||||||
|
|
||||||
|
def compact_json(metadata, context=DEFAULT_CONTEXT):
|
||||||
|
"""
|
||||||
|
Compact json with supplied context.
|
||||||
|
|
||||||
|
Note: Free floating" nodes are removed (eg a key just named
|
||||||
|
"bazzzzzz" which isn't specified in the context... something like
|
||||||
|
bazzzzzz:blerp will stay though. This is jsonld.compact behavior.
|
||||||
|
"""
|
||||||
|
compacted = jsonld.compact(
|
||||||
|
metadata, context,
|
||||||
|
options={
|
||||||
|
"documentLoader": load_context,
|
||||||
|
# This allows for things like "license" and etc to be preserved
|
||||||
|
"expandContext": context,
|
||||||
|
"keepFreeFloatingNodes": False})
|
||||||
|
|
||||||
|
return compacted
|
||||||
|
|
||||||
|
|
||||||
|
def compact_and_validate(metadata, context=DEFAULT_CONTEXT,
|
||||||
|
schema=DEFAULT_SCHEMA):
|
||||||
|
"""
|
||||||
|
compact json with supplied context, check against schema for errors
|
||||||
|
|
||||||
|
raises an exception (jsonschema.exceptions.ValidationError) if
|
||||||
|
there's an error.
|
||||||
|
|
||||||
|
Note: Free floating" nodes are removed (eg a key just named
|
||||||
|
"bazzzzzz" which isn't specified in the context... something like
|
||||||
|
bazzzzzz:blerp will stay though. This is jsonld.compact behavior.
|
||||||
|
|
||||||
|
You may wish to do this validation yourself... this is just for convenience.
|
||||||
|
"""
|
||||||
|
compacted = compact_json(metadata, context)
|
||||||
|
validate(metadata, schema, format_checker=DEFAULT_CHECKER)
|
||||||
|
|
||||||
|
return compacted
|
||||||
|
|
||||||
|
|
||||||
|
def expand_json(metadata, context=DEFAULT_CONTEXT):
|
||||||
|
"""
|
||||||
|
Expand json, but be sure to use our documentLoader.
|
||||||
|
|
||||||
|
By default this expands with DEFAULT_CONTEXT, but if you do not need this,
|
||||||
|
you can safely set this to None.
|
||||||
|
|
||||||
|
# @@: Is the above a good idea? Maybe it should be set to None by
|
||||||
|
# default.
|
||||||
|
"""
|
||||||
|
options = {
|
||||||
|
"documentLoader": load_context}
|
||||||
|
if context is not None:
|
||||||
|
options["expandContext"] = context
|
||||||
|
return jsonld.expand(metadata, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
def rdfa_to_readable(rdfa_predicate):
|
||||||
|
readable = rdfa_predicate.split(u":")[1].capitalize()
|
||||||
|
return readable
|
@ -101,3 +101,7 @@ add_route('mediagoblin.edit.edit_media',
|
|||||||
add_route('mediagoblin.edit.attachments',
|
add_route('mediagoblin.edit.attachments',
|
||||||
'/u/<string:user>/m/<int:media_id>/attachments/',
|
'/u/<string:user>/m/<int:media_id>/attachments/',
|
||||||
'mediagoblin.edit.views:edit_attachments')
|
'mediagoblin.edit.views:edit_attachments')
|
||||||
|
|
||||||
|
add_route('mediagoblin.edit.metadata',
|
||||||
|
'/u/<string:user>/m/<int:media_id>/metadata/',
|
||||||
|
'mediagoblin.edit.views:edit_metadata')
|
||||||
|
3
setup.py
3
setup.py
@ -66,8 +66,11 @@ try:
|
|||||||
'mock',
|
'mock',
|
||||||
'itsdangerous',
|
'itsdangerous',
|
||||||
'pytz',
|
'pytz',
|
||||||
|
'six>=1.4.1',
|
||||||
'oauthlib==0.5.0',
|
'oauthlib==0.5.0',
|
||||||
'unidecode',
|
'unidecode',
|
||||||
|
'jsonschema',
|
||||||
|
'requests',
|
||||||
'ExifRead',
|
'ExifRead',
|
||||||
|
|
||||||
# PLEASE change this when we can; a dependency is forcing us to set this
|
# PLEASE change this when we can; a dependency is forcing us to set this
|
||||||
|
Loading…
x
Reference in New Issue
Block a user