From 04a95150646247abd13992b5c103a6d780d8861b Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Fri, 8 Jul 2011 01:59:44 -0500 Subject: [PATCH 01/15] F360(tagging) - adds tag fields for submission, edit and display --- mediagoblin/edit/forms.py | 2 ++ mediagoblin/edit/views.py | 5 ++++- mediagoblin/submit/forms.py | 1 + mediagoblin/submit/views.py | 2 ++ mediagoblin/templates/mediagoblin/submit/start.html | 1 + mediagoblin/templates/mediagoblin/user_pages/media.html | 4 ++++ 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index d5e7f0a9..b2d575cb 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -25,6 +25,8 @@ class EditForm(wtforms.Form): slug = wtforms.TextField( 'Slug') description = wtforms.TextAreaField('Description of this work') + tags = wtforms.TextField( + 'Tags') class EditProfileForm(wtforms.Form): bio = wtforms.TextAreaField('Bio', diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index a3326b2d..96cb4be3 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -16,6 +16,7 @@ from webob import exc +from string import split from mediagoblin import messages from mediagoblin.util import render_to_response, redirect, clean_html @@ -35,7 +36,8 @@ def edit_media(request, media): form = forms.EditForm(request.POST, title = media['title'], slug = media['slug'], - description = media['description']) + description = media['description'], + tags = ' '.join(media['tags'])) if request.method == 'POST' and form.validate(): # Make sure there isn't already a MediaEntry with such a slug @@ -59,6 +61,7 @@ def edit_media(request, media): media['description'])) media['slug'] = request.POST['slug'] + media['tags'] = split(request.POST['tags']) media.save() return redirect(request, "mediagoblin.user_pages.media_home", diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index 3fd9ea49..0e0fd086 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -24,3 +24,4 @@ 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') diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 4c7476b0..cdd58786 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -16,6 +16,7 @@ from os.path import splitext from cgi import FieldStorage +from string import split from werkzeug.utils import secure_filename @@ -58,6 +59,7 @@ def submit_start(request): entry['media_type'] = u'image' # heh entry['uploader'] = request.user['_id'] + entry['tags'] = split(request.POST.get('tags')) # Save, just so we can get the entry id for the sake of using # it to generate the file path diff --git a/mediagoblin/templates/mediagoblin/submit/start.html b/mediagoblin/templates/mediagoblin/submit/start.html index 50c86afe..7bacb552 100644 --- a/mediagoblin/templates/mediagoblin/submit/start.html +++ b/mediagoblin/templates/mediagoblin/submit/start.html @@ -25,6 +25,7 @@

Submit yer media

{{ wtforms_util.render_field_div(submit_form.file) }} + {{ wtforms_util.render_field_div(submit_form.tags) }} {{ wtforms_util.render_field_div(submit_form.title) }} {{ wtforms_util.render_textarea_div(submit_form.description) }}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 1484cc73..1c263880 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -48,6 +48,10 @@ user= media.uploader().username) }}"> {{- media.uploader().username }}

+ +

+ {{ ' '.join(media.tags) }} +


Comments

From 272469daf5eb53f2302ae3948dde4e40eaf12497 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Sun, 10 Jul 2011 23:36:21 -0500 Subject: [PATCH 02/15] adds index for tag searches by an uploader --- mediagoblin/db/indexes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py index d379a52b..57bd33cd 100644 --- a/mediagoblin/db/indexes.py +++ b/mediagoblin/db/indexes.py @@ -88,6 +88,13 @@ MEDIAENTRY_INDEXES = { # Indexing on uploaders and when media entries are created. # Used for showing a user gallery, etc. 'index': [('uploader', ASCENDING), + ('created', DESCENDING)]}, + + 'uploader_tags_created': { + # Indexing on the media uploader, the associated tags, and timestamp + # Used for showing media items matching a tag search, most recent first. + 'index': [('uploader', ASCENDING), + ('tags', DESCENDING), ('created', DESCENDING)]}} From cdf538bd6163a47b4c4a6326c943b6deaf2c495a Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Tue, 12 Jul 2011 20:06:17 -0500 Subject: [PATCH 03/15] adds filter function to parse and clean tags field input - for some reason the tags are showing up in the media edit form with u'..' and surrounded with []. I don't know why, grr --- mediagoblin/edit/forms.py | 5 +++-- mediagoblin/edit/views.py | 5 +++-- mediagoblin/submit/forms.py | 4 +++- mediagoblin/util.py | 19 ++++++++++++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index b2d575cb..5e3aab96 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -17,6 +17,8 @@ import wtforms +from mediagoblin.util import convert_to_tag_list + class EditForm(wtforms.Form): title = wtforms.TextField( @@ -25,8 +27,7 @@ class EditForm(wtforms.Form): slug = wtforms.TextField( 'Slug') description = wtforms.TextAreaField('Description of this work') - tags = wtforms.TextField( - 'Tags') + tags = wtforms.TextField('Tags', filters=[convert_to_tag_list]) class EditProfileForm(wtforms.Form): bio = wtforms.TextAreaField('Bio', diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 96cb4be3..f5e7f454 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -19,7 +19,8 @@ from webob import exc from string import split from mediagoblin import messages -from mediagoblin.util import render_to_response, redirect, clean_html +from mediagoblin.util import render_to_response, redirect, clean_html, \ + TAGS_DELIMITER from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry @@ -37,7 +38,7 @@ def edit_media(request, media): title = media['title'], slug = media['slug'], description = media['description'], - tags = ' '.join(media['tags'])) + tags = TAGS_DELIMITER.join(media['tags'])) if request.method == 'POST' and form.validate(): # Make sure there isn't already a MediaEntry with such a slug diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index 0e0fd086..e13d5425 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -17,6 +17,8 @@ import wtforms +from mediagoblin.util import convert_to_tag_list + class SubmitStartForm(wtforms.Form): title = wtforms.TextField( @@ -24,4 +26,4 @@ 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') + tags = wtforms.TextField('Tags', filters=[convert_to_tag_list]) diff --git a/mediagoblin/util.py b/mediagoblin/util.py index ab219df0..7ee0a2d5 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -22,6 +22,7 @@ import sys import re import urllib from math import ceil +from string import strip import copy from babel.localedata import exists @@ -369,8 +370,24 @@ def clean_html(html): return HTML_CLEANER.clean_html(html) -MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') +TAGS_DELIMITER = ' ' +def convert_to_tag_list(tag_string): + """ + Filter input from a "tags" field, + + Strips trailing, leading, and internal whitespace, and also converts + the user input into an array of tags + """ + if tag_string: + taglist = [] + stripped_tag_string = ' '.join(tag_string.strip().split()) + for tag in stripped_tag_string.split(TAGS_DELIMITER): + if tag.strip(): taglist.append(tag.strip()) + return taglist + + +MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') def cleaned_markdown_conversion(text): """ From 93e3468a2af92a623a659628a20605025cea9ca7 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Tue, 12 Jul 2011 20:43:16 -0500 Subject: [PATCH 04/15] displays the tags on edit correctly now -before it was running the tags field through the submit filter. that was kind of dumb -removes the filter function from the edit form -adds unicode syntax in the filter function -uses split correctly when saving the edited tags to mongodb --- mediagoblin/edit/forms.py | 2 +- mediagoblin/edit/views.py | 2 +- mediagoblin/util.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index 5e3aab96..e13cfaa9 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -27,7 +27,7 @@ class EditForm(wtforms.Form): slug = wtforms.TextField( 'Slug') description = wtforms.TextAreaField('Description of this work') - tags = wtforms.TextField('Tags', filters=[convert_to_tag_list]) + tags = wtforms.TextField('Tags') class EditProfileForm(wtforms.Form): bio = wtforms.TextAreaField('Bio', diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index f5e7f454..0c4fd735 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -62,7 +62,7 @@ def edit_media(request, media): media['description'])) media['slug'] = request.POST['slug'] - media['tags'] = split(request.POST['tags']) + media['tags'] = request.POST['tags'].split(TAGS_DELIMITER) media.save() return redirect(request, "mediagoblin.user_pages.media_home", diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 7ee0a2d5..4421bec4 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -370,7 +370,7 @@ def clean_html(html): return HTML_CLEANER.clean_html(html) -TAGS_DELIMITER = ' ' +TAGS_DELIMITER = u' ' def convert_to_tag_list(tag_string): """ @@ -381,7 +381,7 @@ def convert_to_tag_list(tag_string): """ if tag_string: taglist = [] - stripped_tag_string = ' '.join(tag_string.strip().split()) + stripped_tag_string = u' '.join(tag_string.strip().split()) for tag in stripped_tag_string.split(TAGS_DELIMITER): if tag.strip(): taglist.append(tag.strip()) return taglist From 6f2e4585cc7475362205a9ddb0e69d6da2b6dc85 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Tue, 12 Jul 2011 22:26:10 -0500 Subject: [PATCH 05/15] uses standard functions instead of form filters and fixes taglist default - seems simpler to use the same tag field processing procedures on media submit and edit, so now processing with a regular function instead of a form filter. Filters run on form load and post by default. - moved tags to sidebar - taglist defaults to [] instead of None - adds case sensitivity toggle --- mediagoblin/edit/forms.py | 2 -- mediagoblin/edit/views.py | 7 ++++--- mediagoblin/submit/forms.py | 4 +--- mediagoblin/submit/views.py | 5 +++-- .../templates/mediagoblin/user_pages/media.html | 9 +++++---- mediagoblin/util.py | 11 ++++++++--- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index e13cfaa9..21c8509a 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -17,8 +17,6 @@ import wtforms -from mediagoblin.util import convert_to_tag_list - class EditForm(wtforms.Form): title = wtforms.TextField( diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 0c4fd735..0432024e 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -19,8 +19,9 @@ from webob import exc from string import split from mediagoblin import messages -from mediagoblin.util import render_to_response, redirect, clean_html, \ - TAGS_DELIMITER +from mediagoblin.util import ( + render_to_response, redirect, clean_html, TAGS_DELIMITER, \ + convert_to_tag_list) from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry @@ -62,7 +63,7 @@ def edit_media(request, media): media['description'])) media['slug'] = request.POST['slug'] - media['tags'] = request.POST['tags'].split(TAGS_DELIMITER) + media['tags'] = convert_to_tag_list(request.POST['tags']) media.save() return redirect(request, "mediagoblin.user_pages.media_home", diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index e13d5425..0e0fd086 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -17,8 +17,6 @@ import wtforms -from mediagoblin.util import convert_to_tag_list - class SubmitStartForm(wtforms.Form): title = wtforms.TextField( @@ -26,4 +24,4 @@ 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', filters=[convert_to_tag_list]) + tags = wtforms.TextField('Tags') diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index cdd58786..46ec4cea 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -21,7 +21,8 @@ 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) 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,7 +60,7 @@ def submit_start(request): entry['media_type'] = u'image' # heh entry['uploader'] = request.user['_id'] - entry['tags'] = split(request.POST.get('tags')) + entry['tags'] = convert_to_tag_list(request.POST.get('tags')) # Save, just so we can get the entry id for the sake of using # it to generate the file path diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 1c263880..47d5db35 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -48,10 +48,6 @@ user= media.uploader().username) }}"> {{- media.uploader().username }}

- -

- {{ ' '.join(media.tags) }} -


Comments

@@ -114,6 +110,11 @@

{% endif %}

+ {% if media.tags %} +

+ {{ ' '.join(media.tags) }} +

+ {% endif %}
{% else %}

Sorry, no such media found.

diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 4421bec4..f2a2793b 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -371,6 +371,7 @@ def clean_html(html): TAGS_DELIMITER = u' ' +TAGS_CASE_SENSITIVE = False def convert_to_tag_list(tag_string): """ @@ -379,12 +380,16 @@ def convert_to_tag_list(tag_string): Strips trailing, leading, and internal whitespace, and also converts the user input into an array of tags """ + taglist = [] if tag_string: - taglist = [] stripped_tag_string = u' '.join(tag_string.strip().split()) for tag in stripped_tag_string.split(TAGS_DELIMITER): - if tag.strip(): taglist.append(tag.strip()) - return taglist + if tag.strip(): + if TAGS_CASE_SENSITIVE: + taglist.append(tag.strip()) + else: + taglist.append(tag.strip().lower()) + return taglist MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') From 4451219560a4d991a8c4d04e9dffa99fb092bd5b Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Tue, 12 Jul 2011 22:52:32 -0500 Subject: [PATCH 06/15] ensures no duplicate tags per media entry --- mediagoblin/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediagoblin/util.py b/mediagoblin/util.py index f2a2793b..951bdd51 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -384,7 +384,7 @@ def convert_to_tag_list(tag_string): if tag_string: stripped_tag_string = u' '.join(tag_string.strip().split()) for tag in stripped_tag_string.split(TAGS_DELIMITER): - if tag.strip(): + if tag.strip() and tag not in taglist: if TAGS_CASE_SENSITIVE: taglist.append(tag.strip()) else: From cc7ff3c50513ae169abab196f32de97af30e6744 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Tue, 12 Jul 2011 23:58:25 -0500 Subject: [PATCH 07/15] enforces maximum tag length with (in)appropriate messaging --- mediagoblin/edit/views.py | 4 +++- mediagoblin/submit/views.py | 4 +++- mediagoblin/util.py | 26 +++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 0432024e..df0a0d52 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -63,7 +63,9 @@ def edit_media(request, media): media['description'])) media['slug'] = request.POST['slug'] - media['tags'] = convert_to_tag_list(request.POST['tags']) + + # Process the user's folksonomy "tags" + media['tags'] = convert_to_tag_list(request) media.save() return redirect(request, "mediagoblin.user_pages.media_home", diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 46ec4cea..bda77b1d 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -60,7 +60,9 @@ def submit_start(request): entry['media_type'] = u'image' # heh entry['uploader'] = request.user['_id'] - entry['tags'] = convert_to_tag_list(request.POST.get('tags')) + + # Process the user's folksonomy "tags" + entry['tags'] = convert_to_tag_list(request) # Save, just so we can get the entry id for the sake of using # it to generate the file path diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 951bdd51..44e64258 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -372,19 +372,39 @@ def clean_html(html): TAGS_DELIMITER = u' ' TAGS_CASE_SENSITIVE = False +TAGS_MAX_LENGTH = 50 -def convert_to_tag_list(tag_string): +def convert_to_tag_list(request): """ - Filter input from a "tags" field, + Filter input from any "tags" field in the session, Strips trailing, leading, and internal whitespace, and also converts - the user input into an array of tags + the "tags" text into an array of tags """ + tag_string = request.POST.get('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(TAGS_DELIMITER): + + # Do not permit duplicate tags if tag.strip() and tag not in taglist: + + # Enforce maximum tag length + if len(tag) > TAGS_MAX_LENGTH: + tag = tag[:TAGS_MAX_LENGTH] + u'...' + messages.add_message( + request, messages.WARNING, \ + u'Tag truncated to ' + unicode(TAGS_MAX_LENGTH) + \ + u' characters.') + messages.add_message( + request, messages.INFO, \ + u'Why the long tag? Seriously.') + if TAGS_CASE_SENSITIVE: taglist.append(tag.strip()) else: From 1580c7c5ceb309134a7c4e0c8ecf89f95cb76273 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 13 Jul 2011 00:22:16 -0500 Subject: [PATCH 08/15] adds index for searching across all users' tagged images --- mediagoblin/db/indexes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py index 57bd33cd..1a2de55f 100644 --- a/mediagoblin/db/indexes.py +++ b/mediagoblin/db/indexes.py @@ -95,6 +95,12 @@ MEDIAENTRY_INDEXES = { # Used for showing media items matching a tag search, most recent first. 'index': [('uploader', ASCENDING), ('tags', DESCENDING), + ('created', DESCENDING)]}, + + 'tags_created': { + # Indexing media tags, and timestamp (across all users) + # This is used for a front page tag search. + 'index': [('tags', DESCENDING), ('created', DESCENDING)]}} From cea8f2b632de13ca9b423ea97ee8f6abf3cd6ecb Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 13 Jul 2011 12:14:18 -0500 Subject: [PATCH 09/15] adds "state" to tags indices --- mediagoblin/db/indexes.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py index 1a2de55f..3440ac2a 100644 --- a/mediagoblin/db/indexes.py +++ b/mediagoblin/db/indexes.py @@ -90,17 +90,19 @@ MEDIAENTRY_INDEXES = { 'index': [('uploader', ASCENDING), ('created', DESCENDING)]}, - 'uploader_tags_created': { - # Indexing on the media uploader, the associated tags, and timestamp + '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': [('uploader', ASCENDING), + 'index': [('state', ASCENDING), + ('uploader', ASCENDING), ('tags', DESCENDING), ('created', DESCENDING)]}, - 'tags_created': { - # Indexing media tags, and timestamp (across all users) + 'state_tags_created': { + # Indexing on processed?, media tags, and timestamp (across all users) # This is used for a front page tag search. - 'index': [('tags', DESCENDING), + 'index': [('state', ASCENDING), + ('tags', DESCENDING), ('created', DESCENDING)]}} From 909371cdceace162af880c275b9e6e70488e3029 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 20 Jul 2011 23:54:32 -0500 Subject: [PATCH 10/15] raises tag length error in form context instead of in message queue --- mediagoblin/edit/forms.py | 5 ++++- mediagoblin/edit/views.py | 4 +--- mediagoblin/submit/forms.py | 5 ++++- mediagoblin/util.py | 35 +++++++++++++++++++++-------------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index 21c8509a..e7a86bba 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): @@ -25,7 +26,9 @@ class EditForm(wtforms.Form): slug = wtforms.TextField( 'Slug') description = wtforms.TextAreaField('Description of this work') - tags = wtforms.TextField('Tags') + 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 df0a0d52..b3d239e1 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -55,6 +55,7 @@ def edit_media(request, media): else: media['title'] = request.POST['title'] media['description'] = request.POST.get('description') + media['tags'] = convert_to_tag_list(request.POST.get('tags')) md = markdown.Markdown( safe_mode = 'escape') @@ -63,9 +64,6 @@ def edit_media(request, media): media['description'])) media['slug'] = request.POST['slug'] - - # Process the user's folksonomy "tags" - media['tags'] = convert_to_tag_list(request) media.save() return redirect(request, "mediagoblin.user_pages.media_home", diff --git a/mediagoblin/submit/forms.py b/mediagoblin/submit/forms.py index 0e0fd086..1a5a7f4e 100644 --- a/mediagoblin/submit/forms.py +++ b/mediagoblin/submit/forms.py @@ -16,6 +16,7 @@ import wtforms +from mediagoblin.util import tag_length_validator, TOO_LONG_TAG_WARNING class SubmitStartForm(wtforms.Form): @@ -24,4 +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') + tags = wtforms.TextField( + 'Tags', + [tag_length_validator]) diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 44e64258..a84e07c4 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -24,6 +24,7 @@ import urllib from math import ceil from string import strip import copy +import wtforms from babel.localedata import exists import jinja2 @@ -374,14 +375,13 @@ TAGS_DELIMITER = u' ' TAGS_CASE_SENSITIVE = False TAGS_MAX_LENGTH = 50 -def convert_to_tag_list(request): +def convert_to_tag_list(tag_string): """ - Filter input from any "tags" field in the session, + 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 """ - tag_string = request.POST.get('tags') taglist = [] if tag_string: @@ -394,17 +394,6 @@ def convert_to_tag_list(request): # Do not permit duplicate tags if tag.strip() and tag not in taglist: - # Enforce maximum tag length - if len(tag) > TAGS_MAX_LENGTH: - tag = tag[:TAGS_MAX_LENGTH] + u'...' - messages.add_message( - request, messages.WARNING, \ - u'Tag truncated to ' + unicode(TAGS_MAX_LENGTH) + \ - u' characters.') - messages.add_message( - request, messages.INFO, \ - u'Why the long tag? Seriously.') - if TAGS_CASE_SENSITIVE: taglist.append(tag.strip()) else: @@ -412,6 +401,24 @@ def convert_to_tag_list(request): return taglist +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(field.data) + too_long_tags = [ + tag for tag in tags + if len(tag) > TAGS_MAX_LENGTH] + + if too_long_tags: + raise wtforms.ValidationError( + TOO_LONG_TAG_WARNING % ( + TAGS_MAX_LENGTH, ', '.join(too_long_tags))) + + MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') def cleaned_markdown_conversion(text): From 10d7496da2f147a30a70304e8be3a579b15fd093 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Mon, 25 Jul 2011 23:46:36 -0500 Subject: [PATCH 11/15] use config_spec.ini to store tag parsing directives --- mediagoblin/config_spec.ini | 5 +++++ mediagoblin/edit/views.py | 6 +++--- mediagoblin/util.py | 15 ++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini index aadf5c21..5aae6439 100644 --- a/mediagoblin/config_spec.ini +++ b/mediagoblin/config_spec.ini @@ -21,6 +21,11 @@ direct_remote_path = string(default="/mgoblin_static/") email_debug_mode = boolean(default=True) email_sender_address = string(default="notice@mediagoblin.example.org") +# tag parsing +tags_delimiter = string(default=",") +tags_case_sensitive = boolean(default=False) +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/edit/views.py b/mediagoblin/edit/views.py index b3d239e1..3193bfa3 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -19,9 +19,9 @@ 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, clean_html, TAGS_DELIMITER, \ - convert_to_tag_list) + render_to_response, redirect, clean_html, convert_to_tag_list) from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry @@ -39,7 +39,7 @@ def edit_media(request, media): title = media['title'], slug = media['slug'], description = media['description'], - tags = TAGS_DELIMITER.join(media['tags'])) + tags = mg_globals.app_config['tags_delimiter'].join(media['tags'])) if request.method == 'POST' and form.validate(): # Make sure there isn't already a MediaEntry with such a slug diff --git a/mediagoblin/util.py b/mediagoblin/util.py index a84e07c4..8bb90acf 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -371,10 +371,6 @@ def clean_html(html): return HTML_CLEANER.clean_html(html) -TAGS_DELIMITER = u' ' -TAGS_CASE_SENSITIVE = False -TAGS_MAX_LENGTH = 50 - def convert_to_tag_list(tag_string): """ Filter input from incoming string containing user tags, @@ -389,12 +385,13 @@ def convert_to_tag_list(tag_string): 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(TAGS_DELIMITER): + for tag in stripped_tag_string.split( + mg_globals.app_config['tags_delimiter']): # Do not permit duplicate tags if tag.strip() and tag not in taglist: - if TAGS_CASE_SENSITIVE: + if mg_globals.app_config['tags_case_sensitive']: taglist.append(tag.strip()) else: taglist.append(tag.strip().lower()) @@ -411,12 +408,12 @@ def tag_length_validator(form, field): tags = convert_to_tag_list(field.data) too_long_tags = [ tag for tag in tags - if len(tag) > TAGS_MAX_LENGTH] + if len(tag) > mg_globals.app_config['tags_max_length']] if too_long_tags: raise wtforms.ValidationError( - TOO_LONG_TAG_WARNING % ( - TAGS_MAX_LENGTH, ', '.join(too_long_tags))) + TOO_LONG_TAG_WARNING % (mg_globals.app_config['tags_max_length'], \ + ', '.join(too_long_tags))) MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape') From 97e4498c10c803c9d239011d4c48efef52673ec3 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Mon, 25 Jul 2011 23:48:51 -0500 Subject: [PATCH 12/15] on submission, use inline error messaging instead of message queue - the function that converts the user's tag string into a list of tags now accepts a string, but the media submit view was still submitting the request object, like we were going to add any errors to the session. Now the submit view passes the tag string --- mediagoblin/submit/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index bda77b1d..edde4400 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -62,7 +62,7 @@ def submit_start(request): entry['uploader'] = request.user['_id'] # Process the user's folksonomy "tags" - entry['tags'] = convert_to_tag_list(request) + entry['tags'] = convert_to_tag_list(request.POST.get('tags')) # Save, just so we can get the entry id for the sake of using # it to generate the file path From 0712a06dc6d6b05cb78d4b10af12c051e8f765e3 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 27 Jul 2011 14:42:09 -0500 Subject: [PATCH 13/15] changes tags to a list of dicts in the db, adding tag slugs - adds a function to convert the tag list of dicts to a text string properly delimited for loading into forms - tag string conversion function updated to generate list of dicts - updates all mentions of the conversion of the string to the tags db object - adds a tags template utility and updates the media template accordingly --- mediagoblin/db/models.py | 2 +- mediagoblin/edit/views.py | 8 ++++-- mediagoblin/submit/views.py | 5 ++-- .../mediagoblin/user_pages/media.html | 4 +-- .../templates/mediagoblin/utils/tags.html | 25 +++++++++++++++++ mediagoblin/util.py | 28 ++++++++++++++----- 6 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 mediagoblin/templates/mediagoblin/utils/tags.html diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 279cb9f2..8fcbb208 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -82,7 +82,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/views.py b/mediagoblin/edit/views.py index 3193bfa3..e4ebe8d7 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -21,7 +21,8 @@ from string import split from mediagoblin import messages from mediagoblin import mg_globals from mediagoblin.util import ( - render_to_response, redirect, clean_html, convert_to_tag_list) + render_to_response, redirect, clean_html, convert_to_tag_list_of_dicts, + media_tags_as_string) from mediagoblin.edit import forms from mediagoblin.edit.lib import may_edit_media from mediagoblin.decorators import require_active_login, get_user_media_entry @@ -39,7 +40,7 @@ def edit_media(request, media): title = media['title'], slug = media['slug'], description = media['description'], - tags = mg_globals.app_config['tags_delimiter'].join(media['tags'])) + 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 @@ -55,7 +56,8 @@ def edit_media(request, media): else: media['title'] = request.POST['title'] media['description'] = request.POST.get('description') - media['tags'] = convert_to_tag_list(request.POST.get('tags')) + media['tags'] = convert_to_tag_list_of_dicts( + request.POST.get('tags')) md = markdown.Markdown( safe_mode = 'escape') diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index edde4400..c5ac8c62 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -22,7 +22,7 @@ from werkzeug.utils import secure_filename from mediagoblin.util import ( render_to_response, redirect, cleaned_markdown_conversion, \ - convert_to_tag_list) + 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 @@ -62,7 +62,8 @@ def submit_start(request): entry['uploader'] = request.user['_id'] # Process the user's folksonomy "tags" - entry['tags'] = convert_to_tag_list(request.POST.get('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 diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 47d5db35..8dd42115 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -111,9 +111,7 @@ {% endif %}

{% if media.tags %} -

- {{ ' '.join(media.tags) }} -

+ {% include "mediagoblin/utils/tags.html" %} {% endif %}
{% else %} 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 . +#} + +{% block tags_content -%} + +{% endblock %} diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 8bb90acf..ab72b5c8 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -371,7 +371,7 @@ def clean_html(html): return HTML_CLEANER.clean_html(html) -def convert_to_tag_list(tag_string): +def convert_to_tag_list_of_dicts(tag_string): """ Filter input from incoming string containing user tags, @@ -389,15 +389,29 @@ def convert_to_tag_list(tag_string): mg_globals.app_config['tags_delimiter']): # Do not permit duplicate tags - if tag.strip() and tag not in taglist: + if tag.strip() and tag.strip() not in taglist: if mg_globals.app_config['tags_case_sensitive']: - taglist.append(tag.strip()) + taglist.append({'name': tag.strip(), + 'slug': slugify(tag.strip())}) else: - taglist.append(tag.strip().lower()) + taglist.append({'name': tag.strip().lower(), + 'slug': slugify(tag.strip().lower())}) 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' @@ -405,10 +419,10 @@ def tag_length_validator(form, field): """ Make sure tags do not exceed the maximum tag length. """ - tags = convert_to_tag_list(field.data) + tags = convert_to_tag_list_of_dicts(field.data) too_long_tags = [ - tag for tag in tags - if len(tag) > mg_globals.app_config['tags_max_length']] + tag['name'] for tag in tags + if len(tag['name']) > mg_globals.app_config['tags_max_length']] if too_long_tags: raise wtforms.ValidationError( From 37be7b6da6aa765991fac55c5c2d0cdb37fd24fc Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 27 Jul 2011 16:04:41 -0500 Subject: [PATCH 14/15] updates indices to index on the slug component of the tag list - uses dot notation to reach into the JSON-style MediaEntry tags field object to index on the slug property of each tag --- mediagoblin/db/indexes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py index 3440ac2a..d0e11311 100644 --- a/mediagoblin/db/indexes.py +++ b/mediagoblin/db/indexes.py @@ -95,14 +95,14 @@ MEDIAENTRY_INDEXES = { # Used for showing media items matching a tag search, most recent first. 'index': [('state', ASCENDING), ('uploader', ASCENDING), - ('tags', DESCENDING), + ('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', DESCENDING), + ('tags.slug', DESCENDING), ('created', DESCENDING)]}} From f99b5caeb68cb60d768f0e049388a6f4a8b68ac0 Mon Sep 17 00:00:00 2001 From: Caleb Forbes Davis V Date: Wed, 27 Jul 2011 23:57:43 -0500 Subject: [PATCH 15/15] modifies duplicate tag check for list of dict tag type change --- mediagoblin/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mediagoblin/util.py b/mediagoblin/util.py index ab72b5c8..f051dc50 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -388,8 +388,8 @@ def convert_to_tag_list_of_dicts(tag_string): for tag in stripped_tag_string.split( mg_globals.app_config['tags_delimiter']): - # Do not permit duplicate tags - if tag.strip() and tag.strip() not in taglist: + # Ignore empty or duplicate tags + if tag.strip() and tag.strip() not in [t['name'] for t in taglist]: if mg_globals.app_config['tags_case_sensitive']: taglist.append({'name': tag.strip(),