Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for context path #313

Merged
merged 2 commits into from
Aug 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ LD_CONTAINER_NAME=linkding
LD_HOST_PORT=9090
# Directory on the host system that should be mounted as data dir into the Docker container
LD_HOST_DATA_DIR=./data
# Context path endswith slash. For example: linkding/
s2marine marked this conversation as resolved.
Show resolved Hide resolved
LD_CONTEXT_PATH=

# Option to disable background tasks
LD_DISABLE_BACKGROUND_TASKS=False
Expand Down
2 changes: 1 addition & 1 deletion bookmarks/templates/bookmarks/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
{% endif %}
<div class="navbar container grid-lg">
<section class="navbar-section">
<a href="/" class="navbar-brand text-bold">
<a href="{% url 'bookmarks:index' %}" class="navbar-brand text-bold">
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
<h1>linkding</h1>
</a>
Expand Down
42 changes: 42 additions & 0 deletions bookmarks/tests/test_context_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import datetime
import email
import importlib
import os
import urllib.parse

from django.test import TestCase, override_settings
from django.urls import reverse, include, resolvers
from django.urls.resolvers import URLResolver, RegexPattern
from django.conf import settings
s2marine marked this conversation as resolved.
Show resolved Hide resolved


class ContextPathTestCase(TestCase):

def setUp(self):
self.bookmarks_urls = importlib.import_module('bookmarks.urls')
self.siteroot_urls = importlib.import_module('siteroot.urls')

@override_settings(LD_CONTEXT_PATH=None)
def tearDown(self):
importlib.reload(self.bookmarks_urls)
importlib.reload(self.siteroot_urls)

@override_settings(LD_CONTEXT_PATH='linkding/')
def test_route_with_context_path(self):
bookmarks_patterns = importlib.reload(self.bookmarks_urls).urlpatterns
bookmarks_match = bookmarks_patterns[0].resolve('linkding/bookmarks')
self.assertEqual(bookmarks_match.url_name, 'index')
s2marine marked this conversation as resolved.
Show resolved Hide resolved

siteroot_patterns = importlib.reload(self.siteroot_urls).urlpatterns
siteroot_match = siteroot_patterns[0].resolve('linkding/login/')
self.assertEqual(siteroot_match.url_name, 'login')

@override_settings(LD_CONTEXT_PATH='')
def test_route_without_context_path(self):
bookmarks_patterns = importlib.reload(self.bookmarks_urls).urlpatterns
bookmarks_match = bookmarks_patterns[0].resolve('bookmarks')
self.assertEqual(bookmarks_match.url_name, 'index')

siteroot_patterns = importlib.reload(self.siteroot_urls).urlpatterns
siteroot_match = siteroot_patterns[0].resolve('login/')
self.assertEqual(siteroot_match.url_name, 'login')
49 changes: 26 additions & 23 deletions bookmarks/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf import settings
from django.conf.urls import url
from django.urls import path, include
from django.views.generic import RedirectView
Expand All @@ -8,27 +9,29 @@

app_name = 'bookmarks'
urlpatterns = [
# Redirect root to bookmarks index
url(r'^$', RedirectView.as_view(pattern_name='bookmarks:index', permanent=False)),
# Bookmarks
path('bookmarks', views.bookmarks.index, name='index'),
path('bookmarks/archived', views.bookmarks.archived, name='archived'),
path('bookmarks/shared', views.bookmarks.shared, name='shared'),
path('bookmarks/new', views.bookmarks.new, name='new'),
path('bookmarks/close', views.bookmarks.close, name='close'),
path('bookmarks/<int:bookmark_id>/edit', views.bookmarks.edit, name='edit'),
path('bookmarks/action', views.bookmarks.action, name='action'),
# Settings
path('settings', views.settings.general, name='settings.index'),
path('settings/general', views.settings.general, name='settings.general'),
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
path('settings/import', views.settings.bookmark_import, name='settings.import'),
path('settings/export', views.settings.bookmark_export, name='settings.export'),
# Toasts
path('toasts/acknowledge', views.toasts.acknowledge, name='toasts.acknowledge'),
# API
path('api/', include(router.urls), name='api'),
# Feeds
path('feeds/<str:feed_key>/all', AllBookmarksFeed(), name='feeds.all'),
path('feeds/<str:feed_key>/unread', UnreadBookmarksFeed(), name='feeds.unread'),
path(settings.LD_CONTEXT_PATH, include([
# Redirect root to bookmarks index
url(r'^$', RedirectView.as_view(pattern_name='bookmarks:index', permanent=False)),
# Bookmarks
path('bookmarks', views.bookmarks.index, name='index'),
path('bookmarks/archived', views.bookmarks.archived, name='archived'),
path('bookmarks/shared', views.bookmarks.shared, name='shared'),
path('bookmarks/new', views.bookmarks.new, name='new'),
path('bookmarks/close', views.bookmarks.close, name='close'),
path('bookmarks/<int:bookmark_id>/edit', views.bookmarks.edit, name='edit'),
path('bookmarks/action', views.bookmarks.action, name='action'),
# Settings
path('settings', views.settings.general, name='settings.index'),
path('settings/general', views.settings.general, name='settings.general'),
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
path('settings/import', views.settings.bookmark_import, name='settings.import'),
path('settings/export', views.settings.bookmark_export, name='settings.export'),
# Toasts
path('toasts/acknowledge', views.toasts.acknowledge, name='toasts.acknowledge'),
# API
path('api/', include(router.urls), name='api'),
# Feeds
path('feeds/<str:feed_key>/all', AllBookmarksFeed(), name='feeds.all'),
path('feeds/<str:feed_key>/unread', UnreadBookmarksFeed(), name='feeds.unread'),
]))
]
2 changes: 1 addition & 1 deletion bookmarks/views/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_ttl_hash(seconds=3600):

@login_required
def integrations(request):
application_url = request.build_absolute_uri("/bookmarks/new")
application_url = request.build_absolute_uri(reverse('bookmarks:new'))
api_token = Token.objects.get_or_create(user=request.user)[0]
feed_token = FeedToken.objects.get_or_create(user=request.user)[0]
all_feed_url = request.build_absolute_uri(reverse('bookmarks:feeds.all', args=[feed_token.key]))
Expand Down
9 changes: 8 additions & 1 deletion docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ Configures the request timeout in the uwsgi application server. This can be usef

Values: Valid port number | Default = `9090`

Allows to set a custom port for the UWSGI server running in the container. While Docker containers have their own IP address namespace and port collisions are impossible to achieve, there are other container solutions that share one. Podman, for example, runs all containers in a pod under one namespace, which results in every port only being allowed to be assigned once. This option allows to set a custom port in order to avoid collisions with other containers.
Allows to set a custom port for the UWSGI server running in the container. While Docker containers have their own IP address namespace and port collisions are impossible to achieve, there are other container solutions that share one. Podman, for example, runs all containers in a pod under one namespace, which results in every port only being allowed to be assigned once. This option allows to set a custom port in order to avoid collisions with other containers.

### `LD_CONTEXT_PATH`

Values: `String` | Default = None

Allows configuring the context path of the website. Useful for setting up Nginx reverse proxy.
The context path must end with a slash. For example: `linkding/`
11 changes: 7 additions & 4 deletions siteroot/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@
},
]

LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = '/bookmarks'
LOGOUT_REDIRECT_URL = '/login'
# Website context path.
LD_CONTEXT_PATH = os.getenv('LD_CONTEXT_PATH', '')

LOGIN_URL = '/' + LD_CONTEXT_PATH + 'login'
LOGIN_REDIRECT_URL = '/' + LD_CONTEXT_PATH + 'bookmarks'
LOGOUT_REDIRECT_URL = '/' + LD_CONTEXT_PATH + 'login'

# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
Expand All @@ -127,7 +130,7 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'
STATIC_URL = '/' + LD_CONTEXT_PATH + 'static/'

# Collect static files in static folder
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
Expand Down
18 changes: 11 additions & 7 deletions siteroot/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib.auth import views as auth_views
from django.urls import path, include
from django.conf import settings
s2marine marked this conversation as resolved.
Show resolved Hide resolved

from bookmarks.admin import linkding_admin_site
from .settings import ALLOW_REGISTRATION, DEBUG

urlpatterns = [
path('admin/', linkding_admin_site.urls),
path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True,
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('change-password/', auth_views.PasswordChangeView.as_view(), name='change_password'),
path('password-change-done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path(settings.LD_CONTEXT_PATH, include([
s2marine marked this conversation as resolved.
Show resolved Hide resolved
path('admin/', linkding_admin_site.urls),
path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True,
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('change-password/', auth_views.PasswordChangeView.as_view(), name='change_password'),
path('password-change-done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
])),
path('', include('bookmarks.urls')),
]

Expand Down
4 changes: 4 additions & 0 deletions uwsgi.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ uid = www-data
gid = www-data
buffer-size = 8192

if-env = LD_CONTEXT_PATH
static-map = /%(_)static=static
endif =

if-env = LD_REQUEST_TIMEOUT
http-timeout = %(_)
socket-timeout = %(_)
Expand Down