Repository: django-cms/django-cms

Issue:
Title.redirect should be a TextField not CharField
URLs can be considerably longer than 255 chars. Here's a rather old post describing what the practical lengths are for various browsers from way back in 2006 and even then, 2000 char URLs are supported by all browsers. That length has likely gotten longer still.

And while it is not every day you find URLS longer than 255 chars, this is precisely why someone might wish to use a redirect in the CMS on a much shorter URL.



Discussion:
Actually, `django` has dedicated database field for storing URLs: `models.URLField` . If changing `Title.redirect` to anything it should be that filed type.

What is more, `models.CharField` can hold more than 255 chars depending on back-end database.

I'm not against your idea of allowing longer URLs but don't agree with proposed field type.

models.URLField does not allow relative urls... does it?

It doesn't by default. I never did custom filed validation but I think it's possible to do this [1] and extend regular expressions to allow relative URLs.

[1] https://docs.djangoproject.com/en/dev/ref/forms/validation/

Here is more appropriate Django documentation for my previous post: https://docs.djangoproject.com/en/dev/ref/validators/

The URLField is implemented in Django as a CharField, which in turn is implemented as a Varchar in MySQL (or, could just be a Char, doesn't change the following). And, in MySQL < 5.0.3, this has a max length of 255, so we may find that some longer URLs won't fit in them on some DB backends anyway.

Regardless, the point of this issue is that we're limiting possible redirects to lengths shorter than some applications may require.

I don't much see the point in using a URLField with a custom validator when we can just use a TextField (with or without our custom validator) and safely avoid requiring MySQL version 5.0.3 or later for Django CMS.

If we're happy that all of our MySQL users will be using 5.0.3 or later, then we could simply modify the field with 'max_length=2048' without actually changing the field type. I believe this will still require a schema-change/migration, which I wasn't comfortable doing in the code base, or I would have already made one of these changes.

While I think that relying to the URLField is a good idea as it helps keeping things light and coherent, I don't think that the bare URLField, unmodified, totally suits our need.

The only advantage I see in using the URLField, is its validation, so if we have to go around it, it's a bit pointless. Is there any other advantage ?

On the MySQL topic, maybe should we document the restriction, stay with the current field type and just raise its `max_length` ? 

Please keep in mind that since Django 1.5, MySQL version 5.0.3 or later is required [1]. I don't know how it works in MySQL because I'm only using PostgreSQL and SQLite where there is no significant difference between character types. One can specify field length far greater then 255 and also doesn't waste space because only needed amount of storage is allocated. Django CMS must respect Django requirements because it's not standalone app.

I'm against using TextField whenever possible because it's the most wide data type where you can place literally everything.

[1] https://docs.djangoproject.com/en/1.6/ref/databases/#mysql-notes

Awesome. OK, I vote we just change the max_length to 2048 and be done with it.

ok so as soon as we drop django 1.4 support (3.1) we could up the max_length here.

+1

:+1: 


Resolving patch:
diff --git a/cms/migrations/0008_auto_20150208_2149.py b/cms/migrations/0008_auto_20150208_2149.py
new file mode 100644
--- /dev/null
+++ b/cms/migrations/0008_auto_20150208_2149.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('cms', '0007_auto_20141028_1559'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='title',
+            name='redirect',
+            field=models.CharField(max_length=2048, null=True, verbose_name='redirect', blank=True),
+            preserve_default=True,
+        ),
+    ]
diff --git a/cms/models/titlemodels.py b/cms/models/titlemodels.py
--- a/cms/models/titlemodels.py
+++ b/cms/models/titlemodels.py
@@ -25,7 +25,7 @@ class Title(models.Model):
     slug = models.SlugField(_("slug"), max_length=255, db_index=True, unique=False)
     path = models.CharField(_("Path"), max_length=255, db_index=True)
     has_url_overwrite = models.BooleanField(_("has url overwrite"), default=False, db_index=True, editable=False)
-    redirect = models.CharField(_("redirect"), max_length=255, blank=True, null=True)
+    redirect = models.CharField(_("redirect"), max_length=2048, blank=True, null=True)
     page = models.ForeignKey(Page, verbose_name=_("page"), related_name="title_set")
     creation_date = models.DateTimeField(_("creation date"), editable=False, default=timezone.now)
 
diff --git a/cms/south_migrations/0074_auto__chg_field_title_redirect.py b/cms/south_migrations/0074_auto__chg_field_title_redirect.py
new file mode 100644
--- /dev/null
+++ b/cms/south_migrations/0074_auto__chg_field_title_redirect.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Changing field 'Title.redirect'
+        db.alter_column(u'cms_title', 'redirect', self.gf('django.db.models.fields.CharField')(max_length=2048, null=True))
+
+    def backwards(self, orm):
+
+        # Changing field 'Title.redirect'
+        db.alter_column(u'cms_title', 'redirect', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'cms.aliaspluginmodel': {
+            'Meta': {'object_name': 'AliasPluginModel', '_ormbases': ['cms.CMSPlugin']},
+            'alias_placeholder': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'alias_placeholder'", 'null': 'True', 'to': "orm['cms.Placeholder']"}),
+            u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}),
+            'plugin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'alias_reference'", 'null': 'True', 'to': "orm['cms.CMSPlugin']"})
+        },
+        'cms.cmsplugin': {
+            'Meta': {'object_name': 'CMSPlugin'},
+            'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
+            'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
+            'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'cms.globalpagepermission': {
+            'Meta': {'object_name': 'GlobalPagePermission'},
+            'can_add': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_change': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_change_advanced_settings': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'can_change_permissions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'can_delete': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_move_page': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_recover_page': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_view': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'sites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['sites.Site']", 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'cms.page': {
+            'Meta': {'ordering': "('path',)", 'unique_together': "(('publisher_is_draft', 'application_namespace'), ('reverse_id', 'site', 'publisher_is_draft'))", 'object_name': 'Page'},
+            'application_namespace': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'application_urls': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'changed_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
+            'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'created_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}),
+            'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'in_navigation': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'is_home': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'languages': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'limit_visibility_in_menu': ('django.db.models.fields.SmallIntegerField', [], {'default': 'None', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'navigation_extenders': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}),
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['cms.Page']"}),
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'placeholders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cms.Placeholder']", 'symmetrical': 'False'}),
+            'publication_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'publication_end_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Page']"}),
+            'reverse_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
+            'revision_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_pages'", 'to': u"orm['sites.Site']"}),
+            'soft_root': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'template': ('django.db.models.fields.CharField', [], {'default': "'INHERIT'", 'max_length': '100'}),
+            'xframe_options': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+        },
+        'cms.pagepermission': {
+            'Meta': {'object_name': 'PagePermission'},
+            'can_add': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_change': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_change_advanced_settings': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'can_change_permissions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'can_delete': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_move_page': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_publish': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'can_view': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'grant_on': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Page']", 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'cms.pageuser': {
+            'Meta': {'object_name': 'PageUser', '_ormbases': [u'auth.User']},
+            'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_users'", 'to': u"orm['auth.User']"}),
+            u'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'cms.pageusergroup': {
+            'Meta': {'object_name': 'PageUserGroup', '_ormbases': [u'auth.Group']},
+            'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_usergroups'", 'to': u"orm['auth.User']"}),
+            u'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'cms.placeholder': {
+            'Meta': {'object_name': 'Placeholder'},
+            'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'cms.placeholderreference': {
+            'Meta': {'object_name': 'PlaceholderReference', '_ormbases': ['cms.CMSPlugin']},
+            u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'placeholder_ref': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'})
+        },
+        'cms.staticplaceholder': {
+            'Meta': {'unique_together': "(('code', 'site'),)", 'object_name': 'StaticPlaceholder'},
+            'code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'creation_method': ('django.db.models.fields.CharField', [], {'default': "'code'", 'max_length': '20', 'blank': 'True'}),
+            'dirty': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'draft': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'static_draft'", 'null': 'True', 'to': "orm['cms.Placeholder']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+            'public': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'static_public'", 'null': 'True', 'to': "orm['cms.Placeholder']"}),
+            'site': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['sites.Site']", 'null': 'True', 'blank': 'True'})
+        },
+        'cms.title': {
+            'Meta': {'unique_together': "(('language', 'page'),)", 'object_name': 'Title'},
+            'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'has_url_overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
+            'menu_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'meta_description': ('django.db.models.fields.TextField', [], {'max_length': '155', 'null': 'True', 'blank': 'True'}),
+            'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'title_set'", 'to': "orm['cms.Page']"}),
+            'page_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Title']"}),
+            'publisher_state': ('django.db.models.fields.SmallIntegerField', [], {'default': '0', 'db_index': 'True'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'cms.usersettings': {
+            'Meta': {'object_name': 'UserSettings'},
+            'clipboard': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'language': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_usersettings'", 'unique': 'True', 'to': u"orm['auth.User']"})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'sites.site': {
+            'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"},
+            'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        }
+    }
+
+    complete_apps = ['cms']
\ No newline at end of file


Test patch: