[592100] Adding slug for documents and use it for internal links

* Also checks for existence of document in parser._getWikiLink
* Add JS support for slug. Uses django's URLify function combined with their jQuery plugin, prepopulate
* Add migration 35 to add the slug column to the model
* Using django's urlquote/urlencode and removing our related sumo tests
This commit is contained in:
Paul Craciunoiu 2010-08-31 17:24:39 -07:00
Родитель 7f5e465ee4
Коммит 28997476c5
18 изменённых файлов: 293 добавлений и 92 удалений

Просмотреть файл

@ -7,6 +7,7 @@ from django import forms
from django.forms.util import ValidationError
from django.conf import settings
from django.http import HttpResponse
from django.utils.http import urlencode
import jingo
import jinja2
@ -15,7 +16,7 @@ from tower import ugettext as _
from forums.models import Forum as DiscussionForum, Thread, Post
from sumo.models import WikiPage, Category
from questions.models import Question
from sumo.utils import paginate, urlencode
from sumo.utils import paginate
from .clients import (QuestionsClient, WikiClient,
DiscussionClient, SearchError)
from .utils import crc32, locale_or_default, sphinx_locale

Просмотреть файл

@ -3,8 +3,9 @@ import urlparse
import datetime
import re
from django.utils.encoding import smart_unicode
from django.conf import settings
from django.utils.encoding import smart_unicode
from django.utils.http import urlencode
import jinja2
from jingo import register, env
@ -14,7 +15,7 @@ from babel.dates import format_date, format_time, format_datetime
from pytz import timezone
from .urlresolvers import reverse
from .utils import urlencode, wiki_to_html
from .utils import wiki_to_html
class DateTimeFormatError(Exception):

Просмотреть файл

@ -61,8 +61,13 @@ class WikiParser(object):
"""
Checks the page exists, and returns its URL, or the URL to create it.
"""
return reverse('wiki.document',
kwargs={'document_slug': link.replace(' ', '+')})
from wiki.models import Document
try:
d = Document.objects.get(title=link)
except Document.DoesNotExist:
from sumo.helpers import urlparams
return urlparams(reverse('wiki.new_document'), title=link)
return d.get_absolute_url()
def hook_internal_link(self, parser, space, name):
"""Parses text and returns internal link."""

Просмотреть файл

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from nose.tools import eq_
from sumo.utils import urlencode
def test_urlencode():
"""Our urlencode is Unicode-safe."""
items = [('q', u'Fran\xe7ais')]
eq_('q=Fran%C3%A7ais', urlencode(items))
items = [('q', u'は「着')]
eq_('q=%E3%81%AF%E3%80%8C%E7%9D%80', urlencode(items))
def test_urlencode_int():
"""urlencode() should not choke on integers."""
items = [('q', 't'), ('a', 1)]
eq_('q=t&a=1', urlencode(items))

Просмотреть файл

@ -52,7 +52,7 @@ class TestWikiParser(TestCase):
"""_buildImageParams handles wiki pages."""
items = ['page=Installing Firefox']
params = self.p._buildImageParams(items)
eq_('/kb/Installing+Firefox', params['link'])
eq_('/en-US/kb/installing-firefox', params['link'])
def test_image_params_link(self):
"""_buildImageParams handles external links."""
@ -64,7 +64,7 @@ class TestWikiParser(TestCase):
"""_buildImageParams - wiki page overrides link."""
items = ['page=Installing Firefox', 'link=http://example.com']
params = self.p._buildImageParams(items)
eq_('/kb/Installing+Firefox', params['link'])
eq_('/en-US/kb/installing-firefox', params['link'])
def test_image_params_align(self):
"""Align valid options."""
@ -116,7 +116,7 @@ class TestWikiParser(TestCase):
def test_get_wiki_link(self):
"""Wiki links are properly built for existing pages."""
eq_('/kb/Installing+Firefox',
eq_('/en-US/kb/installing-firefox',
self.p._getWikiLink('Installing Firefox'))
@ -132,25 +132,25 @@ class TestWikiInternalLinks(TestCase):
def test_simple(self):
"""Simple internal link markup."""
link = pq_link(self.p, '[[Installing Firefox]]')
eq_('/kb/Installing+Firefox', link.attr('href'))
eq_('/en-US/kb/installing-firefox', link.attr('href'))
eq_('Installing Firefox', link.text())
def test_simple_markup(self):
text = '[[Installing Firefox]]'
eq_('<p><a href="/kb/Installing+Firefox" rel="nofollow">' +
eq_('<p><a href="/en-US/kb/installing-firefox" rel="nofollow">' +
'Installing Firefox</a>\n</p>',
self.p.parse(text))
def test_link_hash(self):
"""Internal link with hash."""
link = pq_link(self.p, '[[Installing Firefox#section name]]')
eq_('/kb/Installing+Firefox#section_name', link.attr('href'))
eq_('/en-US/kb/installing-firefox#section_name', link.attr('href'))
eq_('Installing Firefox#section name', link.text())
def test_link_hash_markup(self):
"""Internal link with hash."""
text = '[[Installing Firefox#section name]]'
eq_('<p><a href="/kb/Installing+Firefox#section_name"' +
eq_('<p><a href="/en-US/kb/installing-firefox#section_name"' +
' rel="nofollow">Installing Firefox#section name</a>\n</p>',
self.p.parse(text))
@ -163,12 +163,12 @@ class TestWikiInternalLinks(TestCase):
def test_link_name(self):
"""Internal link with name."""
link = pq_link(self.p, '[[Installing Firefox|this name]]')
eq_('/kb/Installing+Firefox', link.attr('href'))
eq_('/en-US/kb/installing-firefox', link.attr('href'))
eq_('this name', link.text())
def test_link_with_extra_pipe(self):
link = pq_link(self.p, '[[Installing Firefox|with|pipe]]')
eq_('/kb/Installing+Firefox', link.attr('href'))
eq_('/en-US/kb/installing-firefox', link.attr('href'))
eq_('with|pipe', link.text())
def test_hash_name(self):
@ -180,25 +180,25 @@ class TestWikiInternalLinks(TestCase):
def test_link_hash_name(self):
"""Internal link with hash and name."""
link = pq_link(self.p, '[[Installing Firefox#section 3|this name]]')
eq_('/kb/Installing+Firefox#section_3', link.attr('href'))
eq_('/en-US/kb/installing-firefox#section_3', link.attr('href'))
eq_('this name', link.text())
def test_link_hash_name_markup(self):
"""Internal link with hash and name."""
text = '[[Installing Firefox#section 3|this name]]'
eq_('<p><a href="/kb/Installing+Firefox#section_3"' +
eq_('<p><a href="/en-US/kb/installing-firefox#section_3"' +
' rel="nofollow">this name</a>\n</p>', self.p.parse(text))
def test_simple_create(self):
"""Simple link for inexistent page."""
link = pq_link(self.p, '[[A new page]]')
eq_('/kb/A+new+page', link.attr('href'))
eq_('/kb/new?title=A+new+page', link.attr('href'))
eq_('A new page', link.text())
def test_link_edit_hash_name(self):
"""Internal link for inexistent page with hash and name."""
link = pq_link(self.p, '[[A new page#section 3|this name]]')
eq_('/kb/A+new+page#section_3', link.attr('href'))
eq_('/kb/new?title=A+new+page#section_3', link.attr('href'))
eq_('this name', link.text())
@ -248,7 +248,7 @@ class TestWikiImageTags(TestCase):
eq_('file.png', img.attr('alt'))
eq_('file.png', caption)
eq_('/img/wiki_up/file.png', img.attr('src'))
eq_('/kb/Installing+Firefox', img_a.attr('href'))
eq_('/en-US/kb/installing-firefox', img_a.attr('href'))
def test_page_link_edit(self):
"""Link to a nonexistent wiki page."""
@ -260,7 +260,7 @@ class TestWikiImageTags(TestCase):
eq_('file.png', img.attr('alt'))
eq_('file.png', caption)
eq_('/img/wiki_up/file.png', img.attr('src'))
eq_('/kb/Article+List', img_a.attr('href'))
eq_('/kb/new?title=Article+List', img_a.attr('href'))
def test_page_link_caption(self):
"""Link to a wiki page with caption."""
@ -273,7 +273,7 @@ class TestWikiImageTags(TestCase):
eq_('my caption', img.attr('alt'))
eq_('my caption', caption)
eq_('/img/wiki_up/file.png', img.attr('src'))
eq_('/kb/Article+List', img_a.attr('href'))
eq_('/kb/new?title=Article+List', img_a.attr('href'))
def test_link(self):
"""Link to an external page."""
@ -400,4 +400,4 @@ class TestWikiImageTags(TestCase):
self.p, '[[Image:img.png|frameless|page=Installing Firefox]]', 'a')
img = img_a('img')
eq_('frameless', img.attr('class'))
eq_('/kb/Installing+Firefox', img_a.attr('href'))
eq_('/en-US/kb/installing-firefox', img_a.attr('href'))

Просмотреть файл

@ -1,7 +1,5 @@
import urllib
from django.core import paginator
from django.utils.encoding import smart_str
from django.utils.http import urlencode
import jinja2
@ -35,15 +33,6 @@ def paginate(request, queryset, per_page=20):
return paginated
def urlencode(items):
"""A Unicode-safe URLencoder."""
try:
return urllib.urlencode(items)
except UnicodeEncodeError:
return urllib.urlencode([(k, smart_str(v)) for k, v in items])
def wiki_to_html(wiki_markup, wiki_hooks=False):
"""Wiki Markup -> HTML"""
parser = WikiParser(wiki_hooks=wiki_hooks)

Просмотреть файл

@ -13,6 +13,9 @@ KEYWORDS_HELP_TEXT = _lazy(u'Keywords are used to improve searches.')
TITLE_REQUIRED = _lazy(u'Please provide a title.')
TITLE_SHORT = _lazy(u'Your title is too short (%(show_value)s characters). It must be at least %(limit_value)s characters.')
TITLE_LONG = _lazy(u'Please keep the length of your title to %(limit_value)s characters or less. It is currently %(show_value)s characters.')
SLUG_REQUIRED = _lazy(u'Please provide a slug.')
SLUG_SHORT = _lazy(u'Your slug is too short (%(show_value)s characters). It must be at least %(limit_value)s characters.')
SLUG_LONG = _lazy(u'Please keep the length of your slug to %(limit_value)s characters or less. It is currently %(show_value)s characters.')
SUMMARY_REQUIRED = _lazy(u'Please provide a summary.')
SUMMARY_SHORT = _lazy(u'The summary is too short (%(show_value)s characters). It must be at least %(limit_value)s characters.')
SUMMARY_LONG = _lazy(u'Please keep the length of the summary to %(limit_value)s characters or less. It is currently %(show_value)s characters.')
@ -28,6 +31,11 @@ class DocumentForm(forms.ModelForm):
error_messages={'required': TITLE_REQUIRED,
'min_length': TITLE_SHORT,
'max_length': TITLE_LONG})
slug = StrippedCharField(min_length=5, max_length=255,
widget=forms.TextInput(),
error_messages={'required': SLUG_REQUIRED,
'min_length': SLUG_SHORT,
'max_length': SLUG_LONG})
firefox_versions = forms.MultipleChoiceField(
label=_('Firefox Version'),
@ -49,7 +57,7 @@ class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ('title', 'category', 'tags')
fields = ('title', 'slug', 'category', 'tags')
class RevisionForm(forms.ModelForm):

Просмотреть файл

@ -5,9 +5,9 @@ from tower import ugettext_lazy as _lazy
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils.http import urlquote
from sumo.models import ModelBase, TaggableMixin
from sumo.urlresolvers import reverse
from sumo.utils import wiki_to_html
@ -62,6 +62,7 @@ def _inherited(parent_attr, direct_attr):
class Document(ModelBase, TaggableMixin):
"""A localized knowledgebase document, not revision-specific."""
title = models.CharField(max_length=255, db_index=True)
slug = models.CharField(max_length=255, db_index=True)
# TODO: validate (against settings.SUMO_LANGUAGES?)
locale = models.CharField(max_length=7, db_index=True,
@ -94,7 +95,8 @@ class Document(ModelBase, TaggableMixin):
# how MySQL uses indexes, we probably don't need individual indexes on
# title and locale as well as a combined (title, locale) one.
class Meta(object):
unique_together = (('parent', 'locale'), ('title', 'locale'))
unique_together = (('parent', 'locale'), ('title', 'locale'),
('slug', 'locale'))
@property
def content_parsed(self):
@ -106,8 +108,7 @@ class Document(ModelBase, TaggableMixin):
operating_systems = _inherited('operating_systems', 'operating_system_set')
def get_absolute_url(self):
return reverse('wiki.document',
kwargs={'document_slug': self.title.replace(' ', '+')})
return '/%s/kb/%s' % (self.locale, urlquote(self.slug))
def __unicode__(self):
return '[%s] %s' % (self.locale, self.title)

Просмотреть файл

@ -28,7 +28,7 @@
</div>
<div id="actions">
{% if user.has_perm('wiki.add_revision') %}
<a href="{{ url('wiki.new_revision', document.title.replace(' ', '+')) }}">Add a Revision</a>
<a href="{{ url('wiki.new_revision', document.slug) }}">Add a Revision</a>
{% endif %}
</div>
{% endblock %}

Просмотреть файл

@ -1,6 +1,9 @@
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
from django.core.cache import cache
from datetime import datetime
from sumo.tests import LocalizingClient, TestCase
from wiki.models import Document, Revision, CATEGORIES, SIGNIFICANCES
@ -19,9 +22,11 @@ class TestCaseBase(TestCase):
def document(**kwargs):
"""Return an empty document with enough stuff filled out that it can be
saved."""
if 'category' not in kwargs:
kwargs['category'] = CATEGORIES[0][0] # arbitrary
return Document(**kwargs)
auto_title = str(datetime.now())
defaults = {'category': CATEGORIES[0][0], 'title': auto_title}
defaults.update(kwargs)
defaults['slug'] = slugify(defaults['title'])
return Document(**defaults)
def revision(**kwargs):

Просмотреть файл

@ -5,7 +5,7 @@ from pyquery import PyQuery as pq
from sumo.urlresolvers import reverse
from wiki.models import Document, Revision, SIGNIFICANCES, CATEGORIES
from wiki.tests import TestCaseBase
from wiki.tests import TestCaseBase, document, revision
class DocumentTests(TestCaseBase):
@ -48,7 +48,7 @@ class NewDocumentTests(TestCaseBase):
response = self.client.post(reverse('wiki.new_document'), data,
follow=True)
d = Document.objects.get(title=data['title'])
eq_([('http://testserver/en-US/kb/%s/history' % d.id, 302)],
eq_([('http://testserver/en-US/kb/%s/history' % d.slug, 302)],
response.redirect_chain)
eq_(data['category'], d.category)
eq_(tags, list(d.tags.values_list('name', flat=True)))
@ -121,16 +121,14 @@ class NewRevisionTests(TestCaseBase):
"""Trying to create a new revision wihtout permission returns 403."""
d = _create_document()
self.client.login(username='rrosario', password='testpass')
response = self.client.get(reverse('wiki.new_revision',
args=[d.title.replace(' ', '+')]))
response = self.client.get(reverse('wiki.new_revision', args=[d.slug]))
eq_(302, response.status_code)
def test_new_revision_GET_with_perm(self):
"""HTTP GET to new revision URL renders the form."""
d = _create_document()
self.client.login(username='admin', password='testpass')
response = self.client.get(reverse('wiki.new_revision',
args=[d.title.replace(' ', '+')]))
response = self.client.get(reverse('wiki.new_revision', args=[d.slug]))
eq_(200, response.status_code)
doc = pq(response.content)
eq_(1, len(doc('#document-form textarea[name="content"]')))
@ -149,8 +147,7 @@ class NewRevisionTests(TestCaseBase):
r.save()
self.client.login(username='admin', password='testpass')
response = self.client.get(reverse('wiki.new_revision_based_on',
args=[d.title.replace(' ', '+'),
r.id]))
args=[d.slug, r.id]))
eq_(200, response.status_code)
doc = pq(response.content)
eq_(doc('#id_keywords')[0].value, r.keywords)
@ -166,12 +163,10 @@ class NewRevisionTests(TestCaseBase):
"""
d = _create_document()
self.client.login(username='admin', password='testpass')
response = self.client.post(reverse('wiki.new_revision',
args=[d.title.replace(' ', '+')]),
{'summary': 'A brief summary',
'content': 'The article content',
'keywords': 'keyword1 keyword2',
'significance': 10})
response = self.client.post(
reverse('wiki.new_revision', args=[d.slug]),
{'summary': 'A brief summary', 'content': 'The article content',
'keywords': 'keyword1 keyword2', 'significance': 10})
eq_(302, response.status_code)
eq_(2, d.revisions.count())
@ -188,8 +183,7 @@ class NewRevisionTests(TestCaseBase):
self.client.login(username='admin', password='testpass')
tags = ['tag1', 'tag2', 'tag3']
data = _new_document_data(tags)
response = self.client.post(reverse('wiki.new_revision',
args=[d.title.replace(' ', '+')]),
response = self.client.post(reverse('wiki.new_revision', args=[d.slug]),
data)
eq_(302, response.status_code)
eq_(2, d.revisions.count())
@ -226,23 +220,23 @@ class DocumentRevisionsTests(TestCaseBase):
"""Verify the document revisions list view."""
d = _create_document()
user = User.objects.get(pk=118533)
r1 = Revision(summary="a tweak", content='lorem ipsum dolor',
r1 = revision(summary="a tweak", content='lorem ipsum dolor',
significance=10, keywords='kw1 kw2', document=d,
creator=user)
r1.save()
r2 = Revision(summary="another tweak", content='lorem dimsum dolor',
r2 = revision(summary="another tweak", content='lorem dimsum dolor',
significance=10, keywords='kw1 kw2', document=d,
creator=user)
r2.save()
response = self.client.get(reverse('wiki.document_revisions',
args=[d.title.replace(' ', '+')]))
args=[d.slug]))
eq_(200, response.status_code)
doc = pq(response.content)
eq_(3, len(doc('#revision-list > ul > li')))
def _create_document(title='Test Document'):
d = Document(title=title, html='<div>Lorem Ipsum</div>',
d = document(title=title, html='<div>Lorem Ipsum</div>',
category=1, locale='en-US')
d.save()
r = Revision(document=d, keywords='key1, key2', summary='lipsum',
@ -257,6 +251,7 @@ def _create_document(title='Test Document'):
def _new_document_data(tags):
return {
'title': 'A Test Article',
'slug': 'a-test-article',
'tags': ','.join(tags),
'firefox_versions': [1, 2],
'operating_systems': [1, 3],

Просмотреть файл

@ -6,11 +6,12 @@ urlpatterns = patterns('wiki.views',
url(r'^/all$', 'list_documents', name='wiki.all_documents'),
url(r'^/category/(?P<category>\d+)$', 'list_documents',
name='wiki.category'),
url(r'^/(?P<document_slug>[\+\w]+)$', 'document', name='wiki.document'),
url(r'^/(?P<document_slug>[\+\w]+)/history$', 'document_revisions',
name='wiki.document_revisions'),
url(r'^/(?P<document_slug>[\+\w]+)/edit$', 'new_revision',
url(r'^/(?P<document_slug>[^\/]+)$', 'document',
name='wiki.document'),
url(r'^/(?P<document_slug>[^\/]+)/history$',
'document_revisions', name='wiki.document_revisions'),
url(r'^/(?P<document_slug>[^\/]+)/edit$', 'new_revision',
name='wiki.new_revision'),
url(r'^/(?P<document_slug>[\+\w]+)/edit/(?P<revision_id>\d+)$',
url(r'^/(?P<document_slug>[^\/]+)/edit/(?P<revision_id>\d+)$',
'new_revision', name='wiki.new_revision_based_on'),
)

Просмотреть файл

@ -17,7 +17,8 @@ def document(request, document_slug):
"""View a wiki document."""
# This may change depending on how we decide to structure
# the url and handle locales.
doc = get_object_or_404(Document, title=document_slug.replace('+', ' '))
doc = get_object_or_404(
Document, locale=request.locale, slug=document_slug)
return jingo.render(request, 'wiki/document.html',
{'document': doc})
@ -66,7 +67,7 @@ def new_document(request):
rev.save()
return HttpResponseRedirect(reverse('wiki.document_revisions',
args=[doc.id]))
args=[doc.slug]))
return jingo.render(request, 'wiki/new_document.html',
{'document_form': doc_form,
@ -77,7 +78,8 @@ def new_document(request):
@permission_required('wiki.add_revision')
def new_revision(request, document_slug, revision_id=None):
"""Create a new revision of a wiki document."""
doc = get_object_or_404(Document, title=document_slug.replace('+', ' '))
doc = get_object_or_404(
Document, locale=request.locale, slug=document_slug)
if request.method == 'GET':
if revision_id:
@ -158,7 +160,8 @@ def new_revision(request, document_slug, revision_id=None):
def document_revisions(request, document_slug):
"""List all the revisions of a given document."""
doc = get_object_or_404(Document, title=document_slug.replace('+', ' '))
doc = get_object_or_404(
Document, locale=request.locale, slug=document_slug)
revs = Revision.objects.filter(document=doc)
return jingo.render(request, 'wiki/document_revisions.html',
{'revisions': revs,

Просмотреть файл

@ -0,0 +1,43 @@
/*
* Taken from Django's contrib/admin/media/js folder, thanks Django!
* Copyright Django and licensed under BSD, please see django/LICENSE for
* license details.
* Modified slightly to handle fallback to full title if slug is empty
*/
(function($) {
$.fn.prepopulate = function(dependencies, maxLength) {
/*
Depends on urlify.js
Populates a selected field with the values of the dependent fields,
URLifies and shortens the string.
dependencies - selected jQuery object of dependent fields
maxLength - maximum length of the URLify'd string
*/
return this.each(function() {
var field = $(this);
field.data('_changed', false);
field.change(function() {
field.data('_changed', true);
});
var populate = function () {
// Bail if the fields value has changed
if (field.data('_changed') == true) return;
var values = [], field_val, field_val_raw;
dependencies.each(function() {
if ($(this).val().length > 0) {
values.push($(this).val());
}
});
field_val_raw = values.join(' ');
field_val = URLify(field_val_raw, maxLength) ||
field_val_raw;
field.val(field_val);
};
dependencies.keyup(populate).change(populate).focus(populate);
});
};
})(jQuery);

Просмотреть файл

@ -0,0 +1,145 @@
/*
* Taken from Django's contrib/admin/media/js folder, thanks Django!
* Copyright Django and licensed under BSD, please see django/LICENSE for
* license details.
*/
var LATIN_MAP = {
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç':
'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I',
'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö':
'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U',
'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä':
'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó':
'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u',
'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
}
var LATIN_SYMBOLS_MAP = {
'©':'(c)'
}
var GREEK_MAP = {
'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8',
'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p',
'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w',
'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s',
'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i',
'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8',
'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P',
'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W',
'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I',
'Ϋ':'Y'
}
var TURKISH_MAP = {
'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U',
'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G'
}
var RUSSIAN_MAP = {
'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh',
'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o',
'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c',
'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu',
'я':'ya',
'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh',
'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O',
'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C',
'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu',
'Я':'Ya'
}
var UKRAINIAN_MAP = {
'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g'
}
var CZECH_MAP = {
'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u',
'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T',
'Ů':'U', 'Ž':'Z'
}
var POLISH_MAP = {
'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z',
'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'e', 'Ł':'L', 'Ń':'N', 'Ó':'o', 'Ś':'S',
'Ź':'Z', 'Ż':'Z'
}
var LATVIAN_MAP = {
'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n',
'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'i',
'Ķ':'k', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'u', 'Ž':'Z'
}
var ALL_DOWNCODE_MAPS=new Array()
ALL_DOWNCODE_MAPS[0]=LATIN_MAP
ALL_DOWNCODE_MAPS[1]=LATIN_SYMBOLS_MAP
ALL_DOWNCODE_MAPS[2]=GREEK_MAP
ALL_DOWNCODE_MAPS[3]=TURKISH_MAP
ALL_DOWNCODE_MAPS[4]=RUSSIAN_MAP
ALL_DOWNCODE_MAPS[5]=UKRAINIAN_MAP
ALL_DOWNCODE_MAPS[6]=CZECH_MAP
ALL_DOWNCODE_MAPS[7]=POLISH_MAP
ALL_DOWNCODE_MAPS[8]=LATVIAN_MAP
var Downcoder = new Object();
Downcoder.Initialize = function()
{
if (Downcoder.map) // already made
return ;
Downcoder.map ={}
Downcoder.chars = '' ;
for(var i in ALL_DOWNCODE_MAPS)
{
var lookup = ALL_DOWNCODE_MAPS[i]
for (var c in lookup)
{
Downcoder.map[c] = lookup[c] ;
Downcoder.chars += c ;
}
}
Downcoder.regex = new RegExp('[' + Downcoder.chars + ']|[^' + Downcoder.chars + ']+','g') ;
}
downcode= function( slug )
{
Downcoder.Initialize() ;
var downcoded =""
var pieces = slug.match(Downcoder.regex);
if(pieces)
{
for (var i = 0 ; i < pieces.length ; i++)
{
if (pieces[i].length == 1)
{
var mapped = Downcoder.map[pieces[i]] ;
if (mapped != null)
{
downcoded+=mapped;
continue ;
}
}
downcoded+=pieces[i];
}
}
else
{
downcoded = slug;
}
return downcoded;
}
function URLify(s, num_chars) {
// changes, e.g., "Petty theft" to "petty_theft"
// remove all these words from the string before urlifying
s = downcode(s);
removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from",
"is", "in", "into", "like", "of", "off", "on", "onto", "per",
"since", "than", "the", "this", "that", "to", "up", "via",
"with"];
r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi');
s = s.replace(r, '');
// if downcode doesn't hit, the char will be stripped here
s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars
s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces
s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens
s = s.toLowerCase(); // convert to lowercase
return s.substring(0, num_chars);// trim to first num_chars chars
}

18
media/js/wiki.js Normal file
Просмотреть файл

@ -0,0 +1,18 @@
(function () {
var fields = {
title: {
id: '#id_slug',
dependency_ids: ['#id_title'],
dependency_list: ['#id_title'],
maxLength: 50
}
}, field = null;
for (i in fields) {
field = fields[i];
$('#id_slug').addClass('prepopulated_field');
$(field.id).data('dependency_list', field['dependency_list'])
.prepopulate($(field['dependency_ids'].join(',')),
field.maxLength);
};
}());

Просмотреть файл

@ -0,0 +1,3 @@
ALTER TABLE `wiki_document` ADD `slug` varchar(255) NOT NULL;
CREATE INDEX `wiki_document_slug` ON `wiki_document` (`slug`);
CREATE UNIQUE INDEX `slug` ON `wiki_document` (`slug`,`locale`);

Просмотреть файл

@ -305,6 +305,9 @@ MINIFY_BUNDLES = {
'js/markup.js',
),
'wiki': (
'js/libs/django/urlify.js',
'js/libs/django/prepopulate.js',
'js/wiki.js',
),
},
}