Repository: django-extensions/django-extensions

Issue:
AutoSlugField for foreignkey relationships
Thanks for this great library! I use it on every Django project.

I was looking to use foreignkey relationships in my autoslugfield. Something like:

```
class TeamMember(models.Model):
    name = CharField
    team = FK(Team)
    slug = AutoSlugField(populate_from=("team__name", "name")

class Team(models.model):
    name = CharField
```

Would a PR for this be desirable? I was thinking of using something like this StackOverflow answer to lookup the foreignkey relationship: http://stackoverflow.com/questions/20235807/how-to-get-foreign-key-values-with-getattr-from-models
AutoSlugField for foreignkey relationships
Thanks for this great library! I use it on every Django project.

I was looking to use foreignkey relationships in my autoslugfield. Something like:

```
class TeamMember(models.Model):
    name = CharField
    team = FK(Team)
    slug = AutoSlugField(populate_from=("team__name", "name")

class Team(models.model):
    name = CharField
```

Would a PR for this be desirable? I was thinking of using something like this StackOverflow answer to lookup the foreignkey relationship: http://stackoverflow.com/questions/20235807/how-to-get-foreign-key-values-with-getattr-from-models


Discussion:
Thanks !

Sounds good, happy to accept a PR for this.

Maybe you can reuse (some) of the code inside the django ORM for the foreignkey lookup ?
Thanks !

Sounds good, happy to accept a PR for this.

Maybe you can reuse (some) of the code inside the django ORM for the foreignkey lookup ?

Resolving patch:
diff --git a/django_extensions/db/fields/__init__.py b/django_extensions/db/fields/__init__.py
--- a/django_extensions/db/fields/__init__.py
+++ b/django_extensions/db/fields/__init__.py
@@ -22,6 +22,7 @@
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models import DateTimeField, CharField, SlugField
+from django.db.models.constants import LOOKUP_SEP
 from django.template.defaultfilters import slugify
 from django.utils.crypto import get_random_string
 from django.utils.encoding import force_text
@@ -80,7 +81,12 @@ class AutoSlugField(UniqueFieldMixin, SlugField):
     Required arguments:
 
     populate_from
-        Specifies which field or list of fields the slug is populated from.
+        Specifies which field, list of fields, or model method
+        the slug will be populated from.
+
+        populate_from can traverse a ForeignKey relationship
+        by using Django ORM syntax:
+            populate_from = 'related_model__field'
 
     Optional arguments:
 
@@ -150,7 +156,7 @@ def create_slug(self, model_instance, add):
 
         if add or self.overwrite:
             # slugify the original field content and set next step to 2
-            slug_for_field = lambda field: self.slugify_func(getattr(model_instance, field))
+            slug_for_field = lambda lookup_value: self.slugify_func(self.get_slug_fields(model_instance, lookup_value))
             slug = self.separator.join(map(slug_for_field, self._populate_from))
             start = 2
         else:
@@ -175,6 +181,22 @@ def create_slug(self, model_instance, add):
         return super(AutoSlugField, self).find_unique(
             model_instance, slug_field, self.slug_generator(original_slug, start))
 
+    def get_slug_fields(self, model_instance, lookup_value):
+        lookup_value_path = lookup_value.split(LOOKUP_SEP)
+        attr = model_instance
+        for elem in lookup_value_path:
+            try:
+                attr = getattr(attr, elem)
+            except AttributeError:
+                raise AttributeError(
+                    "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format(
+                        elem, lookup_value, attr, elem))
+
+        if callable(attr):
+            return "%s" % attr()
+
+        return attr
+
     def pre_save(self, model_instance, add):
         value = force_text(self.create_slug(model_instance, add))
         return value
diff --git a/django_extensions/mongodb/fields/__init__.py b/django_extensions/mongodb/fields/__init__.py
--- a/django_extensions/mongodb/fields/__init__.py
+++ b/django_extensions/mongodb/fields/__init__.py
@@ -10,6 +10,7 @@
 import six
 import datetime
 from django import forms
+from django.db.models.constants import LOOKUP_SEP
 from django.template.defaultfilters import slugify
 from django.utils.translation import ugettext_lazy as _
 from mongoengine.fields import StringField, DateTimeField
@@ -95,7 +96,7 @@ def create_slug(self, model_instance, add):
 
         if add or self.overwrite:
             # slugify the original field content and set next step to 2
-            slug_for_field = lambda field: self.slugify_func(getattr(model_instance, field))
+            slug_for_field = lambda lookup_value: self.slugify_func(self.get_slug_fields(model_instance, lookup_value))
             slug = self.separator.join(map(slug_for_field, self._populate_from))
             next = 2
         else:
@@ -145,6 +146,22 @@ def create_slug(self, model_instance, add):
             next += 1
         return slug
 
+    def get_slug_fields(self, model_instance, lookup_value):
+        lookup_value_path = lookup_value.split(LOOKUP_SEP)
+        attr = model_instance
+        for elem in lookup_value_path:
+            try:
+                attr = getattr(attr, elem)
+            except AttributeError:
+                raise AttributeError(
+                    "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format(
+                        elem, lookup_value, attr, elem))
+
+        if callable(attr):
+            return "%s" % attr()
+
+        return attr
+
     def pre_save(self, model_instance, add):
         value = six.u(self.create_slug(model_instance, add))
         setattr(model_instance, self.attname, value)


Test patch:
diff --git a/tests/test_autoslug_fields.py b/tests/test_autoslug_fields.py
--- a/tests/test_autoslug_fields.py
+++ b/tests/test_autoslug_fields.py
@@ -10,7 +10,9 @@
 import django_extensions  # noqa
 from django_extensions.db.fields import AutoSlugField
 
-from .testapp.models import ChildSluggedTestModel, SluggedTestModel
+from .testapp.models import ChildSluggedTestModel, SluggedTestModel, \
+    FKSluggedTestModel, FKSluggedTestModelCallable, \
+    ModelMethodSluggedTestModel
 
 
 @pytest.mark.usefixtures("admin_user")
@@ -91,6 +93,18 @@ def test_empty_slug_source(self):
         n.save()
         self.assertEqual(n.slug, '-3')
 
+    def test_callable_slug_source(self):
+        m = ModelMethodSluggedTestModel(title='-foo')
+        m.save()
+        self.assertEqual(m.slug, 'the-title-is-foo')
+
+        n = ModelMethodSluggedTestModel(title='-foo')
+        n.save()
+        self.assertEqual(n.slug, 'the-title-is-foo-2')
+
+        n.save()
+        self.assertEqual(n.slug, 'the-title-is-foo-2')
+
     def test_inheritance_creates_next_slug(self):
         m = SluggedTestModel(title='foo')
         m.save()
@@ -103,6 +117,20 @@ def test_inheritance_creates_next_slug(self):
         o.save()
         self.assertEqual(o.slug, 'foo-3')
 
+    def test_foreign_key_populate_from_field(self):
+        m_fk = SluggedTestModel(title='foo')
+        m_fk.save()
+        m = FKSluggedTestModel(related_field=m_fk)
+        m.save()
+        self.assertEqual(m.slug, 'foo')
+
+    def test_foreign_key_populate_from_callable(self):
+        m_fk = ModelMethodSluggedTestModel(title='foo')
+        m_fk.save()
+        m = FKSluggedTestModelCallable(related_field=m_fk)
+        m.save()
+        self.assertEqual(m.slug, 'the-title-is-foo')
+
 
 class MigrationTest(TestCase):
     def safe_exec(self, string, value=None):
diff --git a/tests/testapp/models.py b/tests/testapp/models.py
--- a/tests/testapp/models.py
+++ b/tests/testapp/models.py
@@ -63,6 +63,33 @@ class Meta:
         app_label = 'django_extensions'
 
 
+class ModelMethodSluggedTestModel(models.Model):
+    title = models.CharField(max_length=42)
+    slug = AutoSlugField(populate_from='get_readable_title')
+
+    class Meta:
+        app_label = 'django_extensions'
+
+    def get_readable_title(self):
+        return "The title is {}".format(self.title)
+
+
+class FKSluggedTestModel(models.Model):
+    related_field = models.ForeignKey(SluggedTestModel)
+    slug = AutoSlugField(populate_from="related_field__title")
+
+    class Meta:
+        app_label = 'django_extensions'
+
+
+class FKSluggedTestModelCallable(models.Model):
+    related_field = models.ForeignKey(ModelMethodSluggedTestModel)
+    slug = AutoSlugField(populate_from="related_field__get_readable_title")
+
+    class Meta:
+        app_label = 'django_extensions'
+
+
 class JSONFieldTestModel(models.Model):
     a = models.IntegerField()
     j_field = JSONField()