Skip to content

Commit

Permalink
feat: endpoint for urls, link widget
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun committed Oct 25, 2024
1 parent 1d33608 commit bcec5e4
Show file tree
Hide file tree
Showing 22 changed files with 902 additions and 296 deletions.
75 changes: 50 additions & 25 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ django CMS Link

|pypi| |build| |coverage|

**django CMS Link** is a plugin for `django CMS <http://django-cms.org>`_ that
**django CMS Link** is a plugin for `django CMS <https://django-cms.org>`_ that
allows you to add links on your site.

This plugin supports child plugins. If you add an other plugin as a
child it will take this content instead of the link name as the content of the link.

This addon is compatible with `Divio Cloud <http://divio.com>`_ and is also available on the
`django CMS Marketplace <https://marketplace.django-cms.org/en/addons/browse/djangocms-link/>`_
for easy installation.
This addon is compatible with `Divio Cloud <http://divio.com>`_.

.. image:: preview.gif

Expand Down Expand Up @@ -43,8 +41,9 @@ file for additional dependencies:

* Django Filer 1.7 or higher

Make sure `django Filer <http://django-filer.readthedocs.io/en/latest/installation.html>`_
is installed and configured appropriately.
If `django Filer <http://django-filer.readthedocs.io/en/latest/installation.html>`_
is installed and configured appropriately, django CMS Link will allow linking
files.


Installation
Expand Down Expand Up @@ -73,7 +72,7 @@ setting:
('feature', _('Featured Version')),
]
You'll need to create the `feature` folder inside ``templates/djangocms_link/``
You'll need to create the ``feature`` folder inside ``templates/djangocms_link/``
otherwise you will get a *template does not exist* error. You can do this by
copying the ``default`` folder inside that directory and renaming it to
``feature``.
Expand All @@ -98,17 +97,33 @@ Either of these might accept a URL such as:
If left undefined, the normal Django URLValidator will be used.


Django Select2
~~~~~~~~~~~~~~
Link fields
-----------

This plugin supports `django-select2 <https://github.com/applegrew/django-select2#installation>`_
for simpler use of internal links. You need to manually enable it by:
As of version 5, django CMS Link provides a re-usable link model field,
form field and form widget. This allows you to use the link field in your own
models or forms.

* run ``pip install django-select2``
* add ``django_select2`` to your ``INSTALLED_APPS``
* add ``url(r'^select2/', include('django_select2.urls')),`` to your ``urls.py``
* set ``DJANGOCMS_LINK_USE_SELECT2 = True`` in your ``settings.py``
.. code-block:: python
from djangocms_link.fields import LinkField, LinkFormField, LinkWidget
class MyModel(models.Model):
link = LinkField()
class MyForm(forms.Form):
link = LinkFormField(required=False)
``LinkField`` is a subclass of ``JSONField`` and stores the link data as dict.
To render the link field in a template, use the new template tags::

{% load djangocms_link_tags %}
<a href="{{ obj.link|to_url }}">Link</a>

{% get_url obj.link as url %}
{% if url %}
<a href="{{ url }}">Link available</a>
{% endif %}

Running Tests
-------------
Expand All @@ -135,18 +150,28 @@ You can run tests by executing::
.. |djangocms| image:: https://img.shields.io/badge/django%20CMS-3.7%2B-blue.svg
:target: https://www.django-cms.org/

Updating from version 4 or lower
--------------------------------

Updating from `cmsplugin-filer <https://github.com/django-cms/cmsplugin-filer>`_
--------------------------------------------------------------------------------

Historically, `cmsplugin-filer` was used to create file, folder, image, link, teaser & video plugins on your django CMS projects. Now `cmsplugin-filer` has been archived, you can still migrate your old instances without having to copy them manually to the new `djangocms-<file|picture|link|...>` plugins.

There's a third-party management command that supports your migration:
django CMS Link 5 is a rewrite of the plugin. If you are updating from
version 4 or lower, you will notice

`migrate_cmsplugin_filer.py <https://gist.github.com/corentinbettiol/84a6ea7e4d047fc01861b0af15fd60f0>`_
* the **new re-usable link widget**, greatly simplifying the user interface
* an **improved management of multi-site situations**, essentially avoiding the
unnecessary additon of the host name to the URL in plugin instances that
are not in a page placeholder (such as links on aliases or static placeholder)
* a **re-usable admin endpoint** for querying available links which can be used
by other apps such as djangocms-text.
* Links are generated by template tags or template filters instead of the
model's ``get_link()`` method. This allows multiple links in future models. The
``get_link()`` method is still available for backwards compatibility.

This management command is only a starting point. It *has* worked out of the box for some people, but we encourage you to read the code, understand what it does, and test it on a development environment before running it on your production server.
Migrations should automatically existing plugin instances to the new model
fields.

The management command is only configured to transfer your `cmsplugin_link`, `cmsplugin_file`, `cmsplugin_folder` and `cmsplugin_image` plugins to modern `djangocms_*` plugins. If you need to transfer other `cmsplugin_*` plugins, you'll have to write your own code.
.. warning::

Alternatively you can use the `deprecate_cmsplugin_filer <https://github.com/ImaginaryLandscape/deprecate_cmsplugin_filer>`_ app, which only adds a small migration that transfer the old `cmsplugin-filer` plugins instances to the new `djangocms-<file|picture|link|...>` plugins.
Migration has worked for some people seamlessly. We strongly recommend to
backup your database before updating to version 5. If you encounter any
issues, please report them on
`GitHub <https://github.com/django-cms/djangocms-link/issues>`_.
144 changes: 144 additions & 0 deletions djangocms_link/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from cms.models import Page, PageContent

Check failure on line 1 in djangocms_link/admin.py

View workflow job for this annotation

GitHub Actions / isort

Imports are incorrectly sorted and/or formatted.
from cms.utils import get_language_from_request
from django.apps import apps
from django.contrib import admin
from django.core.exceptions import PermissionDenied, FieldError
from django.http import JsonResponse, Http404

Check failure on line 6 in djangocms_link/admin.py

View workflow job for this annotation

GitHub Actions / flake8

'django.http.Http404' imported but unused
from django.urls import path
from django.views.generic.list import BaseListView

from cms import __version__

from . import models

_version = int(__version__.split(".")[0])


class UrlsJsonView(BaseListView):
"""Handle AutocompleteWidget's AJAX requests for data."""

paginate_by = 20
admin_site = None

def get(self, request, *args, **kwargs):
"""
Return a JsonResponse with search results as defined in
serialize_result(), by default:
{
results: [{id: "123" text: "foo"}],
pagination: {more: true}
}
"""
if request.GET.get("g"):
# Get name of a reference
return self.get_reference(request)

self.term, self.language = self.process_request(request)

if not self.has_perm(request):
raise PermissionDenied

self.object_list = self.get_queryset()
context = self.get_context_data()
return JsonResponse(
{
"results": [
{
"text": ("Pages"),
"children": [
self.serialize_result(obj) for obj in context["object_list"]
],
},
],
"pagination": {"more": context["page_obj"].has_next()},
}
)

def get_reference(self, request):
try:
model, pk = request.GET.get("g").split(":")
app, model = model.split(".")
model = apps.get_model(app, model)
if hasattr(model, "admin_manager"):
obj = model.admin_manager.get(pk=pk)
else:
obj = model.objects.get(pk=pk)
if isinstance(obj, Page) and _version >= 4:
obj = obj.pagecontent_set(manager="admin_manager").current_content().first()
return JsonResponse(self.serialize_result(obj))
return JsonResponse(self.serialize_result(obj))
except Exception as e:
return JsonResponse({"error": str(e)})

def serialize_result(self, obj):
"""
Convert the provided model object to a dictionary that is added to the
results list.
"""
return {"id": f"cms.page:{obj.page.pk}", "text": str(obj), "url": obj.get_absolute_url()}

def get_queryset(self):
"""Return queryset based on ModelAdmin.get_search_results()."""
if _version >= 4:
try:
# django CMS 4.2+
qs = list(
PageContent.admin_manager.filter(language=self.language, title__icontains=self.term)
.current_content()
.order_by("page__path")
)
except FieldError:
# django CMS 4.0 - 4.1
qs = list(
PageContent.admin_manager.filter(language=self.language, title__icontains=self.term)
.current_content()
.order_by("page__node__path")
)
else:
# django CMS 3
qs = list(
PageContent.objects.filter(language=self.language, title__icontains=self.term).order_by("page__node__path")

Check failure on line 100 in djangocms_link/admin.py

View workflow job for this annotation

GitHub Actions / flake8

line too long (123 > 119 characters)
)
for page_content in qs:
# Patch the missing get_absolute_url method
page_content.get_absolute_url = lambda: page_content.page.get_absolute_url()
return qs


def process_request(self, request):

Check failure on line 108 in djangocms_link/admin.py

View workflow job for this annotation

GitHub Actions / flake8

too many blank lines (2)
"""
Validate request integrity, extract and return request parameters.
"""
term = request.GET.get("term", "").strip("  ").lower()
language = get_language_from_request(request)
return term, language

def has_perm(self, request, obj=None):
"""Check if user has permission to access the related model."""
if obj is None:
return True
model_admin = self.admin_site._registry.get(obj.__class__)
if model_admin is None:
return False
return model_admin.has_view_permission(request, obj=obj)


class LinkAdmin(admin.ModelAdmin):
"""The LinkAdmin class provides the endpoint for getting the urls. It is not visible in the
admin interface."""
def has_module_permission(self, request):
# Remove from admin
return False

def get_urls(self):
return [
path("urls",
self.admin_site.admin_view(self.url_view),
name=f"{self.opts.app_label}_{self.opts.model_name}_urls")
]

def url_view(self, request):
return UrlsJsonView.as_view(admin_site=self)(request)


admin.site.register(models.Link, LinkAdmin)
6 changes: 6 additions & 0 deletions djangocms_link/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig

Check failure on line 1 in djangocms_link/apps.py

View workflow job for this annotation

GitHub Actions / isort

Imports are incorrectly sorted and/or formatted.
from django.utils.translation import gettext_lazy as _

class DjangoCmsLinkConfig(AppConfig):

Check failure on line 4 in djangocms_link/apps.py

View workflow job for this annotation

GitHub Actions / flake8

expected 2 blank lines, found 1
name = "djangocms_link"
verbose_name = _("django CMS Link")
23 changes: 6 additions & 17 deletions djangocms_link/cms_plugins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.contrib.sites.models import Site
from django.contrib.sites.shortcuts import get_current_site
from django.utils.translation import gettext_lazy as _

from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool

from .forms import LinkForm
from .helpers import get_link
from .models import Link


Expand All @@ -19,17 +20,10 @@ class LinkPlugin(CMSPluginBase):
(None, {
'fields': (
'name',
('external_link', 'internal_link'),
'link',
'target',
)
}),
(_('Link settings'), {
'classes': ('collapse',),
'fields': (
('mailto', 'phone'),
('anchor', 'target'),
('file_link'),
),
}),
(_('Advanced settings'), {
'classes': ('collapse',),
'fields': (
Expand All @@ -39,16 +33,11 @@ class LinkPlugin(CMSPluginBase):
}),
]

@classmethod
def get_render_queryset(cls):
queryset = super().get_render_queryset()
return queryset.select_related('internal_link')

def get_render_template(self, context, instance, placeholder):
return f'djangocms_link/{instance.template}/link.html'

def render(self, context, instance, placeholder):
context['link'] = instance.get_link()
context['link'] = get_link(instance.link, getattr(get_current_site(context["request"]), "id", None))
return super().render(context, instance, placeholder)

def get_form(self, request, obj=None, **kwargs):
Expand All @@ -59,7 +48,7 @@ def get_form(self, request, obj=None, **kwargs):
elif self.page and hasattr(self.page, 'site') and self.page.site:
site = self.page.site
else:
site = Site.objects.get_current()
site = get_current_site(request)

class Form(form_class):
def __init__(self, *args, **kwargs):
Expand Down
Loading

0 comments on commit bcec5e4

Please sign in to comment.