Skip to content

Commit f294c81

Browse files
protorotoyakkypre-commit-ci[bot]
authored
Add schema.org support (#166)
* Add schema.org support * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix tests * Split image_protocol tests in two * Fix typo and fix tests * Add schema.org tests --------- Co-authored-by: Iacopo Spalletti <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 818453f commit f294c81

File tree

17 files changed

+891
-70
lines changed

17 files changed

+891
-70
lines changed

changes/76.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add schema.org support

cms_helper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
META_USE_OG_PROPERTIES=True,
1111
META_USE_TWITTER_PROPERTIES=True,
1212
META_USE_SCHEMAORG_PROPERTIES=True,
13+
META_USE_JSON_LD_SCHEMA=True,
1314
FILE_UPLOAD_TEMP_DIR=mkdtemp(),
1415
TEST_RUNNER="app_helper.pytest_runner.PytestTestRunner",
1516
SECRET_KEY="dont-use-me",

docs/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,7 @@
276276

277277
# If true, do not generate a @detailmenu in the "Top" node's menu.
278278
# texinfo_no_detailmenu = False
279+
280+
intersphinx_mapping = {
281+
"django": ("https://django.readthedocs.io/en/latest/", None),
282+
}

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ django meta has two different operating mode:
2525
upgrading
2626
models
2727
views
28+
schema
2829
settings
2930
rendering
3031
extra_tags

docs/modules.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
Package documentation
22
=====================
33

4-
.. automodule:: meta.models
4+
.. automodule:: meta.views
55
:members:
66
:undoc-members:
7-
:show-inheritance:
7+
:private-members:
88

9-
.. automodule:: meta.views
9+
.. automodule:: meta.models
1010
:members:
1111
:undoc-members:
12-
:show-inheritance:
12+
:private-members: _metadata,_schema
1313

1414
.. automodule:: meta.templatetags.meta
1515
:members:
1616
:undoc-members:
17-
:show-inheritance:
17+
:private-members:

docs/schema.rst

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
.. _schema.org:
2+
3+
**********
4+
schema.org
5+
**********
6+
7+
``django-meta`` provides full support for schema.org in JSON-LD format.
8+
9+
schema.org is supported in both :ref:`models` and :ref:`views` framework.
10+
11+
Model-level
12+
-----------
13+
14+
In the same way as basic :py:attr:`~meta.models.ModelMeta._metadata` attribute,
15+
:py:attr:`~meta.models.ModelMeta._schema` exists to resolve and build
16+
the per-object **Schema.org** representation of the current object.
17+
18+
As per :py:attr:`~meta.models.ModelMeta._metadata`, :py:attr:`~meta.models.ModelMeta._schema`
19+
values can contains the name of a method, property or attribute available on the class:
20+
21+
.. _schema.model:
22+
23+
.. code-block:: python
24+
25+
class Blog(ModelMeta, Model)
26+
...
27+
_schema = {
28+
'image': 'get_image_full_url',
29+
'articleBody': 'text',
30+
'articleSection': 'get_categories',
31+
'author': 'get_schema_author',
32+
'copyrightYear': 'copyright_year',
33+
'dateCreated': 'get_date',
34+
'dateModified': 'get_date',
35+
'datePublished': 'date_published',
36+
'headline': 'headline',
37+
'keywords': 'get_keywords',
38+
'description': 'get_description',
39+
'name': 'title',
40+
'url': 'get_full_url',
41+
'mainEntityOfPage': 'get_full_url',
42+
'publisher': 'get_site',
43+
}
44+
45+
46+
47+
View-level
48+
----------
49+
50+
:py:class:`~meta.views.Meta` and :py:class:`~meta.views.MetadataMixin` provides a few API to work with **schema.org**
51+
properties.
52+
53+
.. _schema.get_schema:
54+
55+
MetadataMixin
56+
+++++++++++++
57+
58+
The high level interface is :py:meth:`meta.views.MetadataMixin.get_schema` which works in much the same way as
59+
:py:attr:`meta.models.ModelMeta._schema`.
60+
61+
In :py:meth:`~meta.views.MetadataMixin.get_schema` you must return the whole **schema.org** structure.
62+
63+
For a single object it can look like this:
64+
65+
.. code-block:: python
66+
67+
def get_schema(self, context=None):
68+
return {
69+
'image': self.object.get_image_full_url(),
70+
'articleBody': self.object.text,
71+
'articleSection': self.object.get_categories(),
72+
'author': self.object.get_schema_author(),
73+
'copyrightYear': self.object.date_published.year,
74+
'dateCreated': self.object.get_date(),
75+
'dateModified': self.object.get_date(),
76+
'datePublished': self.object.date_published(),
77+
'headline': self.object.abstract[:50],
78+
'keywords': self.object.get_keywords(),
79+
'description': self.object.get_description(),
80+
'name': self.object.title(),
81+
'url': self.object.get_full_url(),
82+
'mainEntityOfPage': self.object.get_full_url(),
83+
'publisher': self.object.get_site(),
84+
}
85+
86+
87+
.. note:: as it's :py:attr:`~meta.views.Meta.schema` responsibility to convert objects to types suitable for json encoding,
88+
you are not required to put only literal values here. Instances of :py:class:`~meta.views.Meta`, dates, iterables
89+
and dictionaries are allowed.
90+
91+
.. _schema._schema:
92+
93+
Meta
94+
++++
95+
96+
The low level interface is :py:meth:`meta.views.Meta._schema` attribute or (``schema`` argument to :py:class:`~meta.views.Meta`
97+
constructor):
98+
99+
.. code-block:: python
100+
101+
class MyView(View):
102+
103+
def get_context_data(self, **kwargs):
104+
context = super(PostDetailView, self).get_context_data(**kwargs)
105+
context['meta'] = Meta(schema={
106+
'@type': 'Organization',
107+
'name': 'My Publisher',
108+
'logo': Meta(schema={
109+
'@type': 'ImageObject',
110+
'url': self.get_image_full_url()
111+
})
112+
})
113+
return context

meta/models.py

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
import warnings
22
from copy import copy
33

4+
from django.db.models import Manager
5+
from django.utils.functional import cached_property
6+
47
from .settings import get_setting
58
from .utils import get_request, set_request
9+
from .views import FullUrlMixin
610

711
NEED_REQUEST_OBJECT_ERR_MSG = (
812
"Meta models needs request objects when initializing if sites framework "
913
"is not used. See META_USE_SITES setting."
1014
).strip()
1115

1216

13-
class ModelMeta:
17+
class ModelMeta(FullUrlMixin):
1418
"""
1519
Meta information mixin.
1620
"""
1721

1822
_metadata = {}
23+
"""
24+
Metadata configuration dictionary
25+
26+
`_metadata` dict values can be:
27+
28+
* name of object method taking the field name as parameter
29+
* name of object method taking no parameters
30+
* name of object attribute
31+
* name of callable taking the field name as parameter
32+
* name of callable taking no parameters
33+
* literal value
34+
35+
They are checked in the given order: the first that matches is returned.
36+
37+
Callable must be available in the module (i.e.: imported if not defined in the module itself)
38+
"""
1939
_metadata_default = {
2040
"title": False,
2141
"og_title": False,
@@ -49,6 +69,28 @@ class ModelMeta:
4969
"locale": False,
5070
"custom_namespace": get_setting("OG_NAMESPACES"),
5171
}
72+
_schema = {}
73+
"""
74+
schema.org properties dictionary
75+
76+
`_metadata` dict values can be:
77+
78+
* name of object method taking the field name as parameter
79+
* name of object method taking no parameters
80+
* name of object attribute
81+
* name of callable taking the field name as parameter
82+
* name of callable taking no parameters
83+
* literal value
84+
85+
They are checked in the given order: the first that matches is returned.
86+
87+
Callable must be available in the module (i.e.: imported if not defined in the module itself)
88+
89+
If the resulting value is a :py:class:`~meta.models.ModelMeta` or :py:class:`~meta.views.Meta` instance
90+
its schema is set in the schema.org dataset.
91+
92+
See :ref:`a sample implementation <schema.model>`.
93+
"""
5294

5395
def get_meta(self, request=None):
5496
"""
@@ -70,33 +112,41 @@ def _retrieve_data(self, request, metadata):
70112

71113
def _get_meta_value(self, field, value):
72114
"""
73-
Build the data according to a
115+
Build metadata values from :py:attr:`_metadata`
74116
75117
:param field: metadata field name
76118
:param value: provided value
77119
:return: data
78120
"""
121+
122+
def process_value(item):
123+
if isinstance(item, Manager):
124+
return list(item.all())
125+
elif callable(item):
126+
try:
127+
return item(field)
128+
except TypeError:
129+
return item()
130+
else:
131+
return item
132+
79133
if value:
80134
try:
81-
attr = getattr(self, value)
82-
if callable(attr):
83-
try:
84-
return attr(field)
85-
except TypeError:
86-
return attr()
87-
else:
88-
return attr
135+
return process_value(getattr(self, value))
89136
except (AttributeError, TypeError):
90137
return value
91138

92139
def as_meta(self, request=None):
93140
"""
94-
Method that generates the Meta object (from django-meta)
141+
Populates the :py:class:`~meta.views.Meta` object with values from :py:attr:`_metadata`
142+
143+
:param request: optional request object. Used to build the correct URI for linked objects
144+
:return: Meta object
95145
"""
96146
from meta.views import Meta
97147

98148
metadata = self.get_meta(request)
99-
meta = Meta(request=request)
149+
meta = Meta(request=request, obj=self)
100150
for field, data in self._retrieve_data(request, metadata):
101151
setattr(meta, field, data)
102152
for field in ("og_title", "twitter_title", "schemaorg_title"):
@@ -107,8 +157,23 @@ def as_meta(self, request=None):
107157
generaldesc = getattr(meta, "description", False)
108158
if not getattr(meta, field, False) and generaldesc:
109159
setattr(meta, field, generaldesc)
160+
if self._schema:
161+
meta.schema = self.schema
110162
return meta
111163

164+
@cached_property
165+
def schema(self):
166+
"""
167+
Schema.org object description
168+
169+
:return: dict
170+
"""
171+
schema = {}
172+
for field, value in self._schema.items():
173+
if value:
174+
schema[field] = self._get_meta_value(field, value)
175+
return schema
176+
112177
def get_request(self):
113178
"""
114179
Retrieve request from current instance
@@ -176,7 +241,7 @@ def get_meta_protocol(self):
176241
"""
177242
Current http protocol
178243
"""
179-
return get_setting("SITE_PROTOCOL")
244+
return self.get_protocol()
180245

181246
def build_absolute_uri(self, url):
182247
"""
@@ -189,16 +254,11 @@ def build_absolute_uri(self, url):
189254
if not get_setting("USE_SITES"):
190255
raise RuntimeError(NEED_REQUEST_OBJECT_ERR_MSG)
191256

192-
from django.contrib.sites.models import Site
193-
194-
s = Site.objects.get_current()
195-
meta_protocol = self.get_meta_protocol()
196-
if url.startswith("http"):
197-
return url
198-
if s.domain.find("http") > -1:
199-
return "{}{}".format(s.domain, url) # pragma: no cover
200-
else:
201-
if url.startswith("/"):
202-
return "{}://{}{}".format(meta_protocol, s.domain, url)
203-
else:
204-
return "{}://{}/{}".format(meta_protocol, s.domain, url)
257+
return self._get_full_url(url)
258+
259+
def mainEntityOfPage(self):
260+
return {"@type": "WebPage", "@id": self.build_absolute_uri(self.get_absolute_url())}
261+
262+
@property
263+
def _local_key(self):
264+
return "{}:{}:{}".format(self._meta.app_label, self._meta.model_name, self.pk)

meta/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
META_USE_TWITTER_PROPERTIES = False
1313
META_USE_FACEBOOK_PROPERTIES = False
1414
META_USE_SCHEMAORG_PROPERTIES = False
15+
META_USE_JSON_LD_SCHEMA = False
1516
META_USE_SITES = False
1617
META_USE_TITLE_TAG = False
1718
META_OG_NAMESPACES = None
1819

20+
1921
OBJECT_TYPES = (
2022
("Article", _("Article")),
2123
("Website", _("Website")),

meta/templates/meta/meta.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{% spaceless %}
33
{% autoescape off %}
44
{% if meta %}
5+
{% if meta.use_json_ld and meta.schema %}
6+
<script type="application/ld+json">{{ meta.as_json_ld }}</script>
7+
{% endif %}
58
{% if meta.description %}{% meta 'description' meta.description %}{% endif %}
69
{% if meta.keywords %}{% meta_list 'keywords' meta.keywords %}{% endif %}
710
{% if meta.extra_props %}{% meta_extras meta.extra_props %}{% endif %}

0 commit comments

Comments
 (0)