aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/auth/views.py10
-rw-r--r--mediagoblin/config_spec.ini4
-rw-r--r--mediagoblin/db/indexes.py15
-rw-r--r--mediagoblin/db/models.py2
-rw-r--r--mediagoblin/edit/forms.py4
-rw-r--r--mediagoblin/edit/views.py12
-rw-r--r--mediagoblin/submit/forms.py4
-rw-r--r--mediagoblin/submit/views.py8
-rw-r--r--mediagoblin/templates/mediagoblin/submit/start.html1
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html3
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html1
-rw-r--r--mediagoblin/templates/mediagoblin/utils/object_gallery.html6
-rw-r--r--mediagoblin/templates/mediagoblin/utils/tags.html25
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini4
-rw-r--r--mediagoblin/tests/test_submission.py39
-rw-r--r--mediagoblin/tests/test_tags.py50
-rw-r--r--mediagoblin/util.py57
17 files changed, 231 insertions, 14 deletions
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index fb5db870..df7e2a88 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -145,23 +145,19 @@ def verify_email(request):
user['status'] = u'active'
user['email_verified'] = True
user.save()
- verification_successful = True
messages.add_message(
request,
messages.SUCCESS,
('Your email address has been verified. '
'You may now login, edit your profile, and submit images!'))
else:
- verification_successful = False
messages.add_message(request,
messages.ERROR,
'The verification key or user id is incorrect')
- return render_to_response(
- request,
- 'mediagoblin/user_pages/user.html',
- {'user': user,
- 'verification_successful' : verification_successful})
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=request.user['username'])
def resend_activation(request):
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 28be5f34..bbc1f7d6 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -24,6 +24,10 @@ email_sender_address = string(default="notice@mediagoblin.example.org")
# Set to false to disable registrations
allow_registration = boolean(default=True)
+# tag parsing
+tags_delimiter = string(default=",")
+tags_max_length = integer(default=50)
+
# By default not set, but you might want something like:
# "%(here)s/user_dev/templates/"
local_templates = string()
diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py
index a832e013..30d43c98 100644
--- a/mediagoblin/db/indexes.py
+++ b/mediagoblin/db/indexes.py
@@ -90,6 +90,21 @@ MEDIAENTRY_INDEXES = {
# Indexing on uploaders and when media entries are created.
# Used for showing a user gallery, etc.
'index': [('uploader', ASCENDING),
+ ('created', DESCENDING)]},
+
+ 'state_uploader_tags_created': {
+ # Indexing on processed?, media uploader, associated tags, and timestamp
+ # Used for showing media items matching a tag search, most recent first.
+ 'index': [('state', ASCENDING),
+ ('uploader', ASCENDING),
+ ('tags.slug', DESCENDING),
+ ('created', DESCENDING)]},
+
+ 'state_tags_created': {
+ # Indexing on processed?, media tags, and timestamp (across all users)
+ # This is used for a front page tag search.
+ 'index': [('state', ASCENDING),
+ ('tags.slug', DESCENDING),
('created', DESCENDING)]}}
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index bad15aca..4ef2d928 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -184,7 +184,7 @@ class MediaEntry(Document):
'media_type': unicode,
'media_data': dict, # extra data relevant to this media_type
'plugin_data': dict, # plugins can dump stuff here.
- 'tags': [unicode],
+ 'tags': [dict],
'state': unicode,
# For now let's assume there can only be one main file queued
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index 0ed52af1..a1783a72 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -16,6 +16,7 @@
import wtforms
+from mediagoblin.util import tag_length_validator, TOO_LONG_TAG_WARNING
class EditForm(wtforms.Form):
@@ -26,6 +27,9 @@ class EditForm(wtforms.Form):
'Slug',
[wtforms.validators.Required(message="The slug can't be empty")])
description = wtforms.TextAreaField('Description of this work')
+ tags = wtforms.TextField(
+ 'Tags',
+ [tag_length_validator])
class EditProfileForm(wtforms.Form):
bio = wtforms.TextAreaField('Bio',
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index f372fbb9..5cbaadb5 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -16,10 +16,13 @@
from webob import exc
+from string import split
from mediagoblin import messages
+from mediagoblin import mg_globals
from mediagoblin.util import (
- render_to_response, redirect, cleaned_markdown_conversion)
+ render_to_response, redirect, clean_html, convert_to_tag_list_of_dicts,
+ media_tags_as_string, cleaned_markdown_conversion)
from mediagoblin.edit import forms
from mediagoblin.edit.lib import may_edit_media
from mediagoblin.decorators import require_active_login, get_user_media_entry
@@ -34,7 +37,8 @@ def edit_media(request, media):
form = forms.EditForm(request.POST,
title = media['title'],
slug = media['slug'],
- description = media['description'])
+ description = media['description'],
+ tags = media_tags_as_string(media['tags']))
if request.method == 'POST' and form.validate():
# Make sure there isn't already a MediaEntry with such a slug
@@ -50,7 +54,9 @@ def edit_media(request, media):
else:
media['title'] = request.POST['title']
media['description'] = request.POST.get('description')
-
+ media['tags'] = convert_to_tag_list_of_dicts(
+ request.POST.get('tags'))
+
media['description_html'] = cleaned_markdown_conversion(
media['description'])
diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py
index 3fd9ea49..f02c95a6 100644
--- a/mediagoblin/submit/forms.py
+++ b/mediagoblin/submit/forms.py
@@ -16,6 +16,7 @@
import wtforms
+from mediagoblin.util import tag_length_validator
class SubmitStartForm(wtforms.Form):
@@ -24,3 +25,6 @@ class SubmitStartForm(wtforms.Form):
[wtforms.validators.Length(min=0, max=500)])
description = wtforms.TextAreaField('Description of this work')
file = wtforms.FileField('File')
+ tags = wtforms.TextField(
+ 'Tags',
+ [tag_length_validator])
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index 1848f5e5..87e57dda 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -16,11 +16,13 @@
from os.path import splitext
from cgi import FieldStorage
+from string import split
from werkzeug.utils import secure_filename
from mediagoblin.util import (
- render_to_response, redirect, cleaned_markdown_conversion)
+ render_to_response, redirect, cleaned_markdown_conversion, \
+ convert_to_tag_list_of_dicts)
from mediagoblin.decorators import require_active_login
from mediagoblin.submit import forms as submit_forms, security
from mediagoblin.process_media import process_media_initial
@@ -59,6 +61,10 @@ def submit_start(request):
entry['media_type'] = u'image' # heh
entry['uploader'] = request.user['_id']
+ # Process the user's folksonomy "tags"
+ entry['tags'] = convert_to_tag_list_of_dicts(
+ request.POST.get('tags'))
+
# Save, just so we can get the entry id for the sake of using
# it to generate the file path
entry.save(validate=False)
diff --git a/mediagoblin/templates/mediagoblin/submit/start.html b/mediagoblin/templates/mediagoblin/submit/start.html
index 50c86afe..6d00510c 100644
--- a/mediagoblin/templates/mediagoblin/submit/start.html
+++ b/mediagoblin/templates/mediagoblin/submit/start.html
@@ -27,6 +27,7 @@
{{ wtforms_util.render_field_div(submit_form.file) }}
{{ wtforms_util.render_field_div(submit_form.title) }}
{{ wtforms_util.render_textarea_div(submit_form.description) }}
+ {{ wtforms_util.render_field_div(submit_form.tags) }}
<div class="form_submit_buttons">
<input type="submit" value="Submit" class="button" />
</div>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index dc0b6210..7622d6e6 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -116,6 +116,9 @@
</p>
{% endif %}
</p>
+ {% if media.tags %}
+ {% include "mediagoblin/utils/tags.html" %}
+ {% endif %}
</div>
{% else %}
<p>Sorry, no such media found.<p/>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index 7769b8b3..76cf36be 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -80,6 +80,7 @@
<div class="grid_10 omega">
{% set pagination_base_url = user_gallery_url %}
{% include "mediagoblin/utils/object_gallery.html" %}
+ <div class="clear"></div>
<p><a href="{{ user_gallery_url }}">View all of {{ user.username }}'s media</a></p>
<a href={{ request.urlgen(
'mediagoblin.user_pages.atom_feed',
diff --git a/mediagoblin/templates/mediagoblin/utils/object_gallery.html b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
index 2c7a7129..1b1c69f6 100644
--- a/mediagoblin/templates/mediagoblin/utils/object_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
@@ -19,7 +19,7 @@
{% from "mediagoblin/utils/pagination.html" import render_pagination %}
{% block object_gallery_content -%}
- {% if media_entries %}
+ {% if media_entries and media_entries.count() %}
{% for entry in media_entries %}
<div class="media_thumbnail">
<a href="{{ entry.url_for_self(request.urlgen) }}">
@@ -33,5 +33,9 @@
{% else %}
{{ render_pagination(request, pagination) }}
{% endif %}
+ {% else %}
+ <p>
+ <i>There doesn't seem to be any media here yet...</i>
+ </p>
{% endif %}
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/tags.html b/mediagoblin/templates/mediagoblin/utils/tags.html
new file mode 100644
index 00000000..94c4cf69
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/tags.html
@@ -0,0 +1,25 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# 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 tags_content -%}
+ <ul class="mediaentry_tags">
+ {% for tag in media.tags %}
+ <li class="tag">{{ tag['name'] }}</li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini
index fd0f87a4..7716e9ca 100644
--- a/mediagoblin/tests/test_mgoblin_app.ini
+++ b/mediagoblin/tests/test_mgoblin_app.ini
@@ -7,6 +7,10 @@ email_sender_address = "notice@mediagoblin.example.org"
email_debug_mode = true
db_name = __mediagoblin_tests__
+# tag parsing
+tags_delimiter = ","
+tags_max_length = 50
+
# Celery shouldn't be set up by the application as it's setup via
# mediagoblin.init.celery.from_celery
celery_setup_elsewhere = true
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
index 22b6117c..a7248255 100644
--- a/mediagoblin/tests/test_submission.py
+++ b/mediagoblin/tests/test_submission.py
@@ -35,6 +35,9 @@ EVIL_JPG = pkg_resources.resource_filename(
EVIL_PNG = pkg_resources.resource_filename(
'mediagoblin.tests', 'test_submission/evil.png')
+GOOD_TAG_STRING = 'yin,yang'
+BAD_TAG_STRING = 'rage,' + 'f' * 26 + 'u' * 26
+
class TestSubmission:
def setUp(self):
@@ -110,6 +113,42 @@ class TestSubmission:
assert util.TEMPLATE_TEST_CONTEXT.has_key(
'mediagoblin/user_pages/user.html')
+ def test_tags(self):
+ # Good tag string
+ # --------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'Balanced Goblin',
+ 'tags': GOOD_TAG_STRING
+ }, upload_files=[(
+ 'file', GOOD_JPG)])
+
+ # New media entry with correct tags should be created
+ response.follow()
+ context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/user_pages/user.html']
+ request = context['request']
+ media = request.db.MediaEntry.find({'title': 'Balanced Goblin'})[0]
+ assert_equal(media['tags'],
+ [{'name': u'yin', 'slug': u'yin'},
+ {'name': u'yang', 'slug': u'yang'}])
+
+ # Test tags that are too long
+ # ---------------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'Balanced Goblin',
+ 'tags': BAD_TAG_STRING
+ }, upload_files=[(
+ 'file', GOOD_JPG)])
+
+ # Too long error should be raised
+ context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ form = context['submit_form']
+ assert form.tags.errors == [
+ u'Tags must be shorter than 50 characters. Tags that are too long'\
+ ': ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu']
def test_malicious_uploads(self):
# Test non-suppoerted file with non-supported extension
diff --git a/mediagoblin/tests/test_tags.py b/mediagoblin/tests/test_tags.py
new file mode 100644
index 00000000..c2e9fa2b
--- /dev/null
+++ b/mediagoblin/tests/test_tags.py
@@ -0,0 +1,50 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from mediagoblin.tests.tools import setup_fresh_app
+from mediagoblin import util
+from mediagoblin import mg_globals
+
+
+@setup_fresh_app
+def test_list_of_dicts_conversion(test_app):
+ """
+ When the user adds tags to a media entry, the string from the form is
+ converted into a list of tags, where each tag is stored in the database
+ as a dict. Each tag dict should contain the tag's name and slug. Another
+ function performs the reverse operation when populating a form to edit tags.
+ """
+ # Leading, trailing, and internal whitespace should be removed and slugified
+ assert util.convert_to_tag_list_of_dicts('sleep , 6 AM, chainsaw! ') == [
+ {'name': u'sleep', 'slug': u'sleep'},
+ {'name': u'6 AM', 'slug': u'6-am'},
+ {'name': u'chainsaw!', 'slug': u'chainsaw'}]
+
+ # If the user enters two identical tags, record only one of them
+ assert util.convert_to_tag_list_of_dicts('echo,echo') == [{'name': u'echo',
+ 'slug': u'echo'}]
+
+ # Make sure converting the list of dicts to a string works
+ assert util.media_tags_as_string([{'name': u'yin', 'slug': u'yin'},
+ {'name': u'yang', 'slug': u'yang'}]) == \
+ u'yin,yang'
+
+ # If the tag delimiter is a space then we expect different results
+ mg_globals.app_config['tags_delimiter'] = u' '
+ assert util.convert_to_tag_list_of_dicts('unicorn ceramic nazi') == [
+ {'name': u'unicorn', 'slug': u'unicorn'},
+ {'name': u'ceramic', 'slug': u'ceramic'},
+ {'name': u'nazi', 'slug': u'nazi'}]
diff --git a/mediagoblin/util.py b/mediagoblin/util.py
index 1892378c..5880f856 100644
--- a/mediagoblin/util.py
+++ b/mediagoblin/util.py
@@ -25,6 +25,7 @@ import re
import urllib
from math import ceil, floor
import copy
+import wtforms
from babel.localedata import exists
import jinja2
@@ -384,9 +385,63 @@ def clean_html(html):
return HTML_CLEANER.clean_html(html)
-MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape')
+def convert_to_tag_list_of_dicts(tag_string):
+ """
+ Filter input from incoming string containing user tags,
+
+ Strips trailing, leading, and internal whitespace, and also converts
+ the "tags" text into an array of tags
+ """
+ taglist = []
+ if tag_string:
+
+ # Strip out internal, trailing, and leading whitespace
+ stripped_tag_string = u' '.join(tag_string.strip().split())
+
+ # Split the tag string into a list of tags
+ for tag in stripped_tag_string.split(
+ mg_globals.app_config['tags_delimiter']):
+
+ # Ignore empty or duplicate tags
+ if tag.strip() and tag.strip() not in [t['name'] for t in taglist]:
+
+ taglist.append({'name': tag.strip(),
+ 'slug': slugify(tag.strip())})
+ return taglist
+
+
+def media_tags_as_string(media_entry_tags):
+ """
+ Generate a string from a media item's tags, stored as a list of dicts
+
+ This is the opposite of convert_to_tag_list_of_dicts
+ """
+ media_tag_string = ''
+ if media_entry_tags:
+ media_tag_string = mg_globals.app_config['tags_delimiter'].join(
+ [tag['name'] for tag in media_entry_tags])
+ return media_tag_string
+
+TOO_LONG_TAG_WARNING = \
+ u'Tags must be shorter than %s characters. Tags that are too long: %s'
+
+def tag_length_validator(form, field):
+ """
+ Make sure tags do not exceed the maximum tag length.
+ """
+ tags = convert_to_tag_list_of_dicts(field.data)
+ too_long_tags = [
+ tag['name'] for tag in tags
+ if len(tag['name']) > mg_globals.app_config['tags_max_length']]
+
+ if too_long_tags:
+ raise wtforms.ValidationError(
+ TOO_LONG_TAG_WARNING % (mg_globals.app_config['tags_max_length'], \
+ ', '.join(too_long_tags)))
+MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape')
+
def cleaned_markdown_conversion(text):
"""
Take a block of text, run it through MarkDown, and clean its HTML.