Skip to content

Commit

Permalink
Merge pull request #2 from knyghty/model-metadata
Browse files Browse the repository at this point in the history
Add model metadata
  • Loading branch information
knyghty authored Jul 26, 2020
2 parents 6747676 + f2c4066 commit 0680410
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 42 deletions.
37 changes: 31 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Works on all supported Django versions using PostgreSQl, or with any database
on Django 3.1+.

It offers full internationalization support (tags for multiple languages),
content set from object attributes, automatic Opengraph image width and
heights for ``ImageField``, and more.
content set dynamically from object attributes, automatic Opengraph image
width and heights for ``ImageField``, and more.

`Full documentation <https://django-snakeoil.readthedocs.io/en/latest/index.html>`_

Expand All @@ -24,8 +24,33 @@ object, you can use the model abstract base class::

from snakeoil.models import SEOModel

class MyModel(SEOModel):
pass
class Article(SEOModel):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
main_image = models.Imagefield(blank=True, null=True)

@property
def author_name(self):
return

@property
def snakeoil_metadata(self):
metadata = {
"default": [
{
"name": "author",
"content": self.author.get_full_name(),
},
{"property": "og:title", "content": self.title},
]
}
if self.main_image:
metadata.append(
{"property": "og:image", "attribute": "main_image"}
)
return metadata

You can also override these tags in the admin per-object.

For situations where you can't change the model (flatpages, third party apps)
or don't have one at all, there is an ``SEOPath`` model that maps paths to
Expand Down Expand Up @@ -57,8 +82,8 @@ current language is Dutch. This will generate something like:
<!-- build a static URL -->
<meta property="og:image" content="/static/img/default.jpg">
Note that when using static, width and height are not added, but may add
these yourself. For ``ImageField``, this will be added automatically:
Note that when using ``static``, width and height are not added, but you may
add these yourself. For ``ImageField``, this will be added automatically:
.. code-block:: JSON
Expand Down
47 changes: 46 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,16 @@ First, inherit your model from ``SEOModel``::

from snakeoil.models import SEOModel

class MyModel(SEOModel):
class Article(SEOModel):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
main_image = models.Imagefield()

def get_absolute_url(self):
# Snakeoil will use this to find the object
# if you don't pass it in.
return reverse("...")

@property
def author_name(self):
return self.author.get_full_name()
Expand Down Expand Up @@ -93,6 +98,46 @@ key::
For images using ``ImageField`` in an ``og:image``, this will automatically
populate the ``og:image:width`` and ``og:image:height`` properties.

Setting metadata dynamically
----------------------------

Usually, your models will have some metadata stored as model fields or
attributes, and it's a lot of effort to override this for every obejct.
To set per-model metadata based on object attributes, you can define a
property called ``snakeoil_metadata`` on your model::

from snakeoil.models import SEOModel

class Article(SEOModel):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
main_image = models.Imagefield(blank=True, null=True)

@property
def author_name(self):
return

@property
def snakeoil_metadata(self):
metadata = {
"default": [
{
"name": "author",
"content": self.author.get_full_name(),
},
{"property": "og:title", "content": self.title},
]
}
if self.main_image:
metadata.append(
{"property": "og:image", "attribute": "main_image"}
)
return metadata

.. note::
It's important to use ``attribute`` for ``og:image`` so the height and
and width can be set automatically.

Per-URL metadata
================

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ keywords =
long_description = file: README.rst
name = django-snakeoil
url = https://github.com/knyghty/django-snakeoil
version = 1.0
version = 1.1

[options]
include_package_data = true
Expand Down
33 changes: 19 additions & 14 deletions snakeoil/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def _override_tags(base_tags, overriding_tags):
return output


def _collate_meta_tags(meta_tags):
collated_tags = getattr(settings, "SNAKEOIL_DEFAULT_TAGS", {})
def _collate_meta_tags(meta_tags, default_tags):
collated_tags = default_tags
for language, tags in meta_tags.items():
# Simple case, if the language isn't in the tags, dump them all in.
if language not in collated_tags:
Expand Down Expand Up @@ -114,6 +114,7 @@ def _parse_meta_tags(tags, request, obj):
if isinstance(attr, ImageFieldFile):
field = attr
tag["content"] = _get_absolute_file_url(request, field.url)
parsed_tags.append(tag)
if tag.get("property", "") in {"og:image", "og:image:url"}:
width, height = _get_image_dimensions(obj, field)
parsed_tags.append({"property": "og:image:width", "content": width})
Expand All @@ -122,7 +123,7 @@ def _parse_meta_tags(tags, request, obj):
)
else:
tag["content"] = attr
parsed_tags.append(tag)
parsed_tags.append(tag)
elif "static" in tag:
tag["content"] = _get_absolute_file_url(request, static(tag["static"]))
parsed_tags.append(tag)
Expand All @@ -141,25 +142,29 @@ def get_meta_tags(context, obj=None):
2. If not, try to find the object in the context.
3. If there isn't one, check if there is an object for the current path.
4. Grab the defaults and merge in the tags from the model.
5. Get tags based on the language.
6. Return the tags.
5. Merge in tags from the object.
6. Get tags based on the language.
7. Return the tags.
The priority works like this.
The priority works like this:
- More specific languages beat less specific ones, e.g. en_GB > en > default.
- Tags from the object beat tags from the settings.
"""
try:
if obj is not None:
meta_tags = obj.meta_tags
found_tags = obj.meta_tags
else:
request_path = context["request"].path
obj, meta_tags = _get_meta_tags_from_context(context, request_path)
if not meta_tags:
meta_tags = _get_meta_tags_for_path(request_path)

meta_tags = _collate_meta_tags(meta_tags)
meta_tags = _get_meta_tags_for_language(meta_tags)
meta_tags = _parse_meta_tags(meta_tags, request=context["request"], obj=obj)
obj, found_tags = _get_meta_tags_from_context(context, request_path)
if not found_tags:
found_tags = _get_meta_tags_for_path(request_path)

default_tags = getattr(settings, "SNAKEOIL_DEFAULT_TAGS", {})
model_tags = getattr(obj, "snakeoil_metadata", None) or {}
collated_tags = _collate_meta_tags(model_tags, default_tags)
collated_tags = _collate_meta_tags(found_tags, collated_tags)
collated_tags = _get_meta_tags_for_language(collated_tags)
meta_tags = _parse_meta_tags(collated_tags, request=context["request"], obj=obj)
return {"meta_tags": meta_tags}
except Exception:
logger.exception("Failed fetching meta tags")
Expand Down
18 changes: 16 additions & 2 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,23 @@ class Article(SEOModel):
secondary_image = models.ImageField(null=True, blank=True)
content = models.TextField()

def get_absolute_url(self):
return reverse("article_detail", args=[self.slug])

@property
def author_name(self):
return self.author.get_full_name()

def get_absolute_url(self):
return reverse("article_detail", args=[self.slug])
@property
def snakeoil_metadata(self):
metadata = {
"default": [
{"name": "author", "attribute": "author_name"},
{"property": "og:title", "attribute": "title"},
],
}
if self.main_image:
metadata["default"].append(
{"property": "og:image", "attribute": "main_image"}
)
return metadata
47 changes: 29 additions & 18 deletions tests/test_template_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@


class MetaTemplateTagTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
def setUp(self):
self.user = User.objects.create_user(
username="tom", first_name="Tom", last_name="Carrick"
)
cls.article = Article.objects.create(
author=cls.user,
self.article = Article.objects.create(
author=self.user,
slug="an-article",
title="A test article",
meta_tags={
Expand All @@ -31,6 +30,11 @@ def setUpTestData(cls):
],
},
)
with open(settings.TESTS_DIR / "data" / "kitties.jpg", "rb") as f:
self.article.main_image = SimpleUploadedFile(
name="kitties.jpg", content=f.read(), content_type="image/jpeg",
)
self.article.save()

def test_meta_template_tag_with_seo_model(self):
response = self.client.get(f"/articles/{self.article.slug}/")
Expand All @@ -45,11 +49,6 @@ def test_meta_template_tag_with_seo_model(self):
)

def test_meta_template_tag_with_attr(self):
self.article.meta_tags["en"].append(
{"name": "author", "attribute": "author_name"}
)
self.article.save()

response = self.client.get(f"/articles/{self.article.slug}/")

self.assertTemplateUsed(response, "tests/article_detail.html")
Expand All @@ -58,11 +57,6 @@ def test_meta_template_tag_with_attr(self):
)

def test_attr_with_object_from_context(self):
self.article.meta_tags["en"].append(
{"name": "author", "attribute": "author_name"}
)
self.article.save()

response = self.client.get(
f"/articles/{self.article.slug}/", {"template_without_obj": True}
)
Expand Down Expand Up @@ -140,9 +134,6 @@ def test_image_field_with_width_and_height_fields(self):
self.article.main_image = SimpleUploadedFile(
name="kitties.jpg", content=f.read(), content_type="image/jpeg",
)
self.article.meta_tags["en"] = [
{"property": "og:image", "attribute": "main_image"}
]
self.article.save()

response = self.client.get(f"/articles/{self.article.slug}/")
Expand Down Expand Up @@ -295,3 +286,23 @@ def test_seo_path_root_url(self):
self.assertContains(
response, '<meta name="description" content="home page">', html=True
)

def test_model_metadata(self):
response = self.client.get(f"/articles/{self.article.slug}/")

self.assertContains(
response, '<meta name="author" content="Tom Carrick">', html=True,
)
self.assertContains(
response,
f'<meta property="og:title" content="{self.article.title}">',
html=True,
)
self.assertContains(
response,
(
'<meta property="og:image" '
f'content="http://testserver{self.article.main_image.url}">'
),
html=True,
)

0 comments on commit 0680410

Please sign in to comment.