aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/source/siteadmin/commandline-upload.rst51
-rw-r--r--mediagoblin/edit/forms.py9
-rw-r--r--mediagoblin/edit/views.py37
-rw-r--r--mediagoblin/gmg_commands/batchaddmedia.py137
-rw-r--r--mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html6
-rw-r--r--mediagoblin/static/css/base.css36
-rw-r--r--mediagoblin/templates/mediagoblin/edit/metadata.html97
-rw-r--r--mediagoblin/templates/mediagoblin/utils/wtforms.html61
-rw-r--r--mediagoblin/user_pages/routing.py4
9 files changed, 346 insertions, 92 deletions
diff --git a/docs/source/siteadmin/commandline-upload.rst b/docs/source/siteadmin/commandline-upload.rst
index be19df58..d67c19dd 100644
--- a/docs/source/siteadmin/commandline-upload.rst
+++ b/docs/source/siteadmin/commandline-upload.rst
@@ -39,3 +39,54 @@ 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 media:location column
+-------------------------
+The media: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 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 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. If this is left blank, the media entry will be named according to the filename of the file being uploaded.
+- **dc:description** sets a description of your media entry. If this is left blank the media entry's description will not be filled in.
+- **dc:rights** will set a license for your media entry `if` the data provided is a valid URI. If this is left blank 'All Rights Reserved' will be selected.
+
+You can of course, change these values later.
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index 2c9b5e99..c2355980 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -122,3 +122,12 @@ class ChangeEmailForm(wtforms.Form):
[wtforms.validators.Required()],
description=_(
"Enter your password to prove you own this account."))
+
+class MetaDataForm(wtforms.Form):
+ identifier = wtforms.TextField(_(u'Identifier'))
+ value = wtforms.TextField(_(u'Value'))
+
+class EditMetaDataForm(wtforms.Form):
+ media_metadata = wtforms.FieldList(
+ wtforms.FormField(MetaDataForm, label="")
+ )
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 80590875..34021257 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -17,6 +17,7 @@
from datetime import datetime
from itsdangerous import BadSignature
+from pyld import jsonld
from werkzeug.exceptions import Forbidden
from werkzeug.utils import secure_filename
@@ -29,8 +30,10 @@ 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
from mediagoblin.tools.mail import email_debug_message
from mediagoblin.tools.response import (render_to_response,
redirect, redirect_obj, render_404)
@@ -432,3 +435,35 @@ 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 = compact_and_validate(metadata_dict)
+ media.media_metadata = json_ld_metadata
+ media.save()
+ return redirect_obj(request, media)
+
+ if media.media_metadata:
+ for identifier, value in media.media_metadata.iteritems():
+ if identifier == "@context": continue
+ form.media_metadata.append_entry({
+ 'identifier':identifier,
+ 'value':value})
+ else:
+ form.media_metadata.append_entry({
+ 'identifier':"",
+ 'value':""})
+ form.media_metadata.append_entry({
+ 'identifier':"",
+ 'value':""})
+ return render_to_response(
+ request,
+ 'mediagoblin/edit/metadata.html',
+ {'form':form,
+ 'media':media})
diff --git a/mediagoblin/gmg_commands/batchaddmedia.py b/mediagoblin/gmg_commands/batchaddmedia.py
index e540e88c..75e7b7c5 100644
--- a/mediagoblin/gmg_commands/batchaddmedia.py
+++ b/mediagoblin/gmg_commands/batchaddmedia.py
@@ -25,32 +25,28 @@ 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="Name of user these media entries belong to")
+ help=_(u"Name of user these media entries belong to"))
subparser.add_argument(
- 'target_path',
- help=("""\
-Path to a local archive or directory containing a "location.csv" and a
-"metadata.csv" file. These are csv (comma seperated value) files with the
-locations and metadata of the files to be uploaded. The location must be listed
-with either the URL of the remote media file or the filesystem path of a local
-file. The metadata should be provided with one column for each of the 15 Dublin
-Core properties (http://dublincore.org/documents/dces/). Both "location.csv" and
-"metadata.csv" must begin with a row demonstrating the order of the columns. We
-have provided an example of these files at <url to be added>
-"""))
+ 'metadata_path',
+ help=_(
+u"""Path to the csv file containing metadata information."""))
subparser.add_argument(
'--celery',
action='store_true',
- help="Don't process eagerly, pass off to celery")
+ help=_(u"Don't process eagerly, pass off to celery"))
def batchaddmedia(args):
@@ -65,48 +61,24 @@ def batchaddmedia(args):
# get the user
user = app.db.User.query.filter_by(username=args.username.lower()).first()
if user is None:
- print "Sorry, no user by username '%s' exists" % args.username
+ 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.isdir(args.target_path):
- dir_path = args.target_path
-
- elif tarfile.is_tarfile(args.target_path):
- dir_path = tempfile.mkdtemp()
- temp_files.append(dir_path)
- tar = tarfile.open(args.target_path)
- tar.extractall(path=dir_path)
-
- elif zipfile.is_zipfile(args.target_path):
- dir_path = tempfile.mkdtemp()
- temp_files.append(dir_path)
- zipped_file = zipfile.ZipFile(args.target_path)
- zipped_file.extractall(path=dir_path)
+ if os.path.isfile(args.metadata_path):
+ metadata_path = args.metadata_path
else:
- print "Couldn't recognize the file. This script only accepts tar files,\
-zip files and directories"
- if dir_path.endswith('/'):
- dir_path = dir_path[:-1]
-
- location_file_path = os.path.join(dir_path,"location.csv")
- metadata_file_path = os.path.join(dir_path, "metadata.csv")
-
- # check for the location file, if it exists...
- abs_location_filename = os.path.abspath(location_file_path)
- if not os.path.exists(abs_location_filename):
- print "Can't find a file with filename '%s'" % location_file_path
- return
-
- # check for the metadata file, if it exists...
- abs_metadata_filename = os.path.abspath(metadata_file_path)
- if not os.path.exists(abs_metadata_filename):
- print "Can't find a file with filename '%s'" % metadata_file_path
+ 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):
@@ -116,30 +88,38 @@ zip files and directories"
else:
return unicode(some_string)
- with file(abs_location_filename, 'r') as all_locations:
- contents = all_locations.read()
- media_locations = parse_csv_file(contents)
-
with file(abs_metadata_filename, 'r') as all_metadata:
contents = all_metadata.read()
media_metadata = parse_csv_file(contents)
- for media_id in media_locations.keys():
+ for media_id, file_metadata in media_metadata.iteritems():
files_attempted += 1
-
- file_metadata = media_metadata[media_id]
+ # 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['media:location']
+ file_metadata = dict([(key, value)
+ for key, value in file_metadata.iteritems() if
+ key.split(":")[0] != 'media'])
try:
json_ld_metadata = compact_and_validate(file_metadata)
except ValidationError, exc:
- print "Error with '%s' value '%s': %s" % (
- media_id, exc.path[0], exc.message)
+ 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
- original_location = media_locations[media_id]['media:original']
url = urlparse(original_location)
- title = json_ld_metadata.get('dcterms:title')
- description = json_ld_metadata.get('dcterms:description')
+ ### Pull the important media information for mediagoblin from the
+ ### metadata, if it is provided.
+ title = json_ld_metadata.get('dc:title')
+ description = json_ld_metadata.get('dc:description')
license = json_ld_metadata.get('license')
filename = url.path.split()[-1]
@@ -153,14 +133,14 @@ zip files and directories"
if os.path.isabs(path):
file_abs_path = os.path.abspath(path)
else:
- file_path = os.path.join(dir_path, path)
+ 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 "\
-FAIL: Local file {filename} could not be accessed.".format(filename=filename)
- print "Skipping it."
+ print _(u"""\
+FAIL: Local file {filename} could not be accessed.
+{filename} will not be uploaded.""".format(filename=filename))
continue
try:
submit_media(
@@ -174,29 +154,36 @@ FAIL: Local file {filename} could not be accessed.".format(filename=filename)
metadata=json_ld_metadata,
tags_string=u"",
upload_limit=upload_limit, max_file_size=max_file_size)
- print "Successfully uploading {filename}!".format(filename=filename)
- print ""
+ 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 "FAIL: This file is larger than the upload limits for this site."
+ 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."
+ 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 uploaded".format(
+ 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)
- teardown(temp_files)
+ 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 'media: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 dictionaryfrom mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+ # Build a dictionary
for line in lines:
if line.isspace() or line == '': continue
values = csv_reader([line]).next()
@@ -207,7 +194,3 @@ def parse_csv_file(file_contents):
return objects_dict
-
-def teardown(temp_files):
- for temp_file in temp_files:
- subprocess.call(['rm','-r',temp_file])
diff --git a/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html b/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html
index db12f149..73b5ec52 100644
--- a/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html
+++ b/mediagoblin/plugins/metadata_display/templates/mediagoblin/plugins/metadata_display/metadata_table.html
@@ -32,3 +32,9 @@
{%- 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 %}
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 32c6c6cb..df0e850b 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -610,6 +610,24 @@ a img.media_image {
cursor: zoom-in;
}
+table.metadata_info {
+ font-size:85%;
+ margin-left:10px;
+}
+
+table.metadata_info tr.highlight {
+ color:#f7f7f7;
+}
+
+table.metadata_info th {
+ font-weight: bold;
+ border-spacing: 10px;
+ text-align: left;
+}
+table.metadata_info td {
+ padding: 4px 8px;
+}
+
/* icons */
img.media_icon {
@@ -938,3 +956,21 @@ 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: 1000px;
+}
+
+table.metadata_editor tr th {
+ width:100px;
+}
+
+table.metadata_editor tr td {
+ width:300px;
+}
+table.metadata_editor tr td.form_field_input input {
+ width:300px;
+}
diff --git a/mediagoblin/templates/mediagoblin/edit/metadata.html b/mediagoblin/templates/mediagoblin/edit/metadata.html
new file mode 100644
index 00000000..b5a52e5f
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/edit/metadata.html
@@ -0,0 +1,97 @@
+{#
+# 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 %}Data{% 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>
+ <th></th>
+ <td><input type=button value="{% trans %}Add new Row{% endtrans %}"
+ class="button_action" id="add_new_metadata_row" />
+ </td>
+ <th></th>
+ <td><input type=submit value="{% trans %}Update Metadata{% endtrans %}"
+ class="button_action_highlight" /></td>
+ </tr>
+ <tr>
+ <th></th>
+ <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 %}
diff --git a/mediagoblin/templates/mediagoblin/utils/wtforms.html b/mediagoblin/templates/mediagoblin/utils/wtforms.html
index e079274e..c83d53f1 100644
--- a/mediagoblin/templates/mediagoblin/utils/wtforms.html
+++ b/mediagoblin/templates/mediagoblin/utils/wtforms.html
@@ -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 %}
+ <th>{{ render_label_p(field) }}</th>
+ <td class="form_field_input">
+ {{field}}
+ {%- if field.errors -%}
+ <br />
+ <ul class="errors">
+ {% for error in field.errors %}
+ <li>{{error}}</li>
+ {%- endfor %}
+ </ul>
+ {%- endif -%}
+ </td>
+ {%- endfor %}
+ </tr>
+{%- endmacro %}
+
+{% macro render_field_as_table_row(field) %}
+ <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>
+{% 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">
diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py
index f0f4d8b7..8eb51c8d 100644
--- a/mediagoblin/user_pages/routing.py
+++ b/mediagoblin/user_pages/routing.py
@@ -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')