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
|
||||
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)
|
||||
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
||||
Privilege)
|
||||
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
@ -720,3 +721,15 @@ def drop_MediaEntry_collected(db):
|
||||
media_collected.drop()
|
||||
|
||||
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"
|
||||
)
|
||||
collections = association_proxy("collections_helper", "in_collection")
|
||||
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
|
||||
default=MutationDict())
|
||||
|
||||
## TODO
|
||||
# fail_error
|
||||
|
@ -15,10 +15,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import wtforms
|
||||
from jsonschema import Draft4Validator
|
||||
|
||||
from mediagoblin.tools.text import tag_length_validator
|
||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
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
|
||||
|
||||
|
||||
@ -122,3 +124,38 @@ class ChangeEmailForm(wtforms.Form):
|
||||
[wtforms.validators.Required()],
|
||||
description=_(
|
||||
"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 itsdangerous import BadSignature
|
||||
from pyld import jsonld
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from werkzeug.utils import secure_filename
|
||||
from jsonschema import ValidationError, Draft4Validator
|
||||
|
||||
from mediagoblin import messages
|
||||
from mediagoblin import mg_globals
|
||||
@ -29,8 +31,11 @@ from mediagoblin.edit import forms
|
||||
from mediagoblin.edit.lib import may_edit_media
|
||||
from mediagoblin.decorators import (require_active_login, active_user_from_url,
|
||||
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.metadata import (compact_and_validate, DEFAULT_CHECKER,
|
||||
DEFAULT_SCHEMA)
|
||||
from mediagoblin.tools.mail import email_debug_message
|
||||
from mediagoblin.tools.response import (render_to_response,
|
||||
redirect, redirect_obj, render_404)
|
||||
@ -432,3 +437,30 @@ def change_email(request):
|
||||
'mediagoblin/edit/change_email.html',
|
||||
{'form': form,
|
||||
'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',
|
||||
'func': 'mediagoblin.gmg_commands.deletemedia:deletemedia',
|
||||
'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': {
|
||||
# 'setup': 'mediagoblin.gmg_commands.theme:theme_parser_setup',
|
||||
# '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():
|
||||
add_route('index', '/', 'mediagoblin.views:root_view')
|
||||
add_route('terms_of_service','/terms_of_service',
|
||||
'mediagoblin.views:terms_of_service')
|
||||
'mediagoblin.views:terms_of_service'),
|
||||
mount('/auth', auth_routes)
|
||||
mount('/mod', moderation_routes)
|
||||
|
||||
|
@ -609,7 +609,6 @@ a img.media_image {
|
||||
cursor: -moz-zoom-in;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
/* icons */
|
||||
|
||||
img.media_icon {
|
||||
@ -938,3 +937,16 @@ p.verifier {
|
||||
none repeat scroll 0% 0% rgb(221, 221, 221);
|
||||
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,
|
||||
title=None, description=None,
|
||||
license=None, tags_string=u"",
|
||||
license=None, metadata=None, tags_string=u"",
|
||||
upload_limit=None, max_file_size=None,
|
||||
callback_url=None,
|
||||
# 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.media_metadata = metadata or {}
|
||||
|
||||
# Process the user's folksonomy "tags"
|
||||
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") %}
|
||||
|
||||
{% block mediagoblin_sidebar %}
|
||||
{% endblock %}
|
||||
|
||||
</div><!--end media_sidebar-->
|
||||
|
||||
<div class="clear"></div>
|
||||
|
@ -70,23 +70,56 @@
|
||||
{# Auto-render a form as a table #}
|
||||
{% macro render_table(form) -%}
|
||||
{% for field in form %}
|
||||
<tr>
|
||||
<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>
|
||||
render_field_as_table_row(field)
|
||||
{% endfor %}
|
||||
{%- 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 #}
|
||||
{% macro render_bool(field) %}
|
||||
<div class="boolean">
|
||||
|
@ -14,11 +14,11 @@
|
||||
# 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 urlparse
|
||||
import urlparse, os, pytest
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.db.models import User
|
||||
from mediagoblin.tests.tools import fixture_add_user
|
||||
from mediagoblin.db.models import User, MediaEntry
|
||||
from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry
|
||||
from mediagoblin import auth
|
||||
from mediagoblin.tools import template, mail
|
||||
|
||||
@ -174,3 +174,81 @@ class TestUserEdit(object):
|
||||
email = User.query.filter_by(username='chris').first().email
|
||||
assert email == 'new@example.com'
|
||||
# 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',
|
||||
'/u/<string:user>/m/<int:media_id>/attachments/',
|
||||
'mediagoblin.edit.views:edit_attachments')
|
||||
|
||||
add_route('mediagoblin.edit.metadata',
|
||||
'/u/<string:user>/m/<int:media_id>/metadata/',
|
||||
'mediagoblin.edit.views:edit_metadata')
|
||||
|
Loading…
x
Reference in New Issue
Block a user