Merge branch 'metadata'

This commit is contained in:
tilly-Q
2014-05-14 13:02:30 -04:00
25 changed files with 1134 additions and 24 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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
}

View 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

View File

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

View File

@@ -0,0 +1,3 @@
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('css/metadata_display.css',
'metadata_display') }}"/>

View File

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

View File

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

View File

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

View File

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

View 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"
}
}

View File

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

View 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 %}

View File

@@ -230,9 +230,6 @@
{% template_hook("media_sideinfo") %}
{% block mediagoblin_sidebar %}
{% endblock %}
</div><!--end media_sidebar-->
<div class="clear"></div>

View File

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

View File

@@ -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&#39;On the worst day&#39; is not a &#39;date-time&#39;" in
response.body)

View 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

View 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

View File

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