Skip to content

Commit 710a574

Browse files
committed
Merge branch 'miscs'
2 parents 35a34b0 + f65828b commit 710a574

File tree

16 files changed

+156
-42
lines changed

16 files changed

+156
-42
lines changed

Diff for: CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Add link to changelog in footer.
6+
- Improve existing links in footer.
7+
- Allow you to add a custom script.
8+
- Can fetch feed without an explicit full site URL.
9+
- Can force feeds to refresh in the admin.
10+
- Can refresh a reading list no mobile easily, without going to the top of the page or opening the reading list selector.
11+
312
## 24.12.4
413

514
- Use the theme (light or dark) that matches the system theme.

Diff for: config/settings.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -626,11 +626,12 @@ def before_send_to_sentry(event, hint):
626626
NINJA_PAGINATION_CLASS = "legadilo.utils.pagination.LimitOffsetPagination"
627627

628628

629-
# Your stuff...
629+
# Legadilo's specific stuff...
630630
# ------------------------------------------------------------------------------
631631
ARTICLE_FETCH_TIMEOUT = env.int("LEGADILO_ARTICLE_FETCH_TIMEOUT", default=50)
632632
RSS_FETCH_TIMEOUT = env.int("LEGADILO_RSS_FETCH_TIMEOUT", default=300)
633633
CONTACT_EMAIL = env.str("LEGADILO_CONTACT_EMAIL", default=None)
634+
CUSTOM_SCRIPT = env.json("LEGADILO_CUSTOM_SCRIPT", default=None)
634635
TOKEN_LENGTH = 50
635636
JWT_ALGORITHM = "HS256"
636637
ACCESS_TOKEN_MAX_AGE = timedelta(hours=24)

Diff for: docs/deploy.md

+18-18
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ These variables are required for the container to start and create the proper da
104104
| `DJANGO_DEFAULT_FROM_EMAIL` | Email used as from unless otherwise specified. |
105105
| `ADMIN_URL` | To change the default admin URL in production for security reason. Will default to `/admin/` in dev. |
106106
| `ACCOUNT_ALLOW_REGISTRATION` | Whether to enable account registration on the instance or not. If disabled, you will have to create the user in the Django admin or with the CLI. |
107-
| `DATABASE_URL` | The URL to the database formatted like this: postgres://POSTGRES_USER:POSTGRES_PASSWORD@POSTGRES_HOST:POSTGRES_PORT/POSTGRES_DB |
107+
| `DATABASE_URL` | The URL to the database formatted like this: `postgres://POSTGRES_USER:POSTGRES_PASSWORD@POSTGRES_HOST:POSTGRES_PORT/POSTGRES_DB` |
108108

109109
```{admonition} DATABASE_URL
110110
:class: note
@@ -122,20 +122,20 @@ You can create cryptahpicaly sure secrets in Python with `python3 -c "import sec
122122

123123
### Other variables
124124

125-
| Variable name | Default value | Description |
126-
|-----------------------------------------|--------------------|----------------------------------------------------------------------------------------|
127-
| `DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS` | `True` | See https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains |
128-
| `DJANGO_SECURE_HSTS_PRELOAD` | `True` | See https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload |
129-
| `DJANGO_SERVER_EMAIL` | DEFAULT_FROM_EMAIL | The email address that error messages come from. |
130-
| `DJANGO_EMAIL_SUBJECT_PREFIX` | `[Legadilo]` | Each email will be prefixed by this. |
131-
| `EMAIL_HOST` | `mailpit` | On which host to connect to send an email. Leave the default to not send in production |
132-
| `EMAIL_PORT` | 1025 | On which port to connect to send an email. |
133-
| `EMAIL_HOST_USER` | Empty string | Username to use for the SMTP server defined in `EMAIL_HOST` |
134-
| `EMAIL_HOST_PASSWORD` | Empty string | The password associated with the above username |
135-
| `EMAIL_TIMEOUT` | 30 | Max time to wait for when trying to send an email before failing. |
136-
| `EMAIL_USE_TLS` | False | Whether to use TLS to send email with SMTP |
137-
| `SENTRY_DSN` | `None` | To enable error monitoring with Sentry (leave empty to leave it deactivated). |
138-
| `LEGADILO_ARTICLE_FETCH_TIMEOUT` | 50 | The fetch timeout when fetching articles in seconds. |
139-
| `LEGADILO_RSS_FETCH_TIMEOUT` | 300 | The fetch timeout when fetching feeds in seconds. |
140-
| `LEGADILO_CONTACT_EMAIL` | `None` | The contact email to display to authenticated user. |
141-
125+
| Variable name | Default value | Description |
126+
|-----------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
127+
| `DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS` | `True` | See https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains |
128+
| `DJANGO_SECURE_HSTS_PRELOAD` | `True` | See https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload |
129+
| `DJANGO_SERVER_EMAIL` | DEFAULT_FROM_EMAIL | The email address that error messages come from. |
130+
| `DJANGO_EMAIL_SUBJECT_PREFIX` | `[Legadilo]` | Each email will be prefixed by this. |
131+
| `EMAIL_HOST` | `mailpit` | On which host to connect to send an email. Leave the default to not send in production |
132+
| `EMAIL_PORT` | 1025 | On which port to connect to send an email. |
133+
| `EMAIL_HOST_USER` | Empty string | Username to use for the SMTP server defined in `EMAIL_HOST` |
134+
| `EMAIL_HOST_PASSWORD` | Empty string | The password associated with the above username |
135+
| `EMAIL_TIMEOUT` | 30 | Max time to wait for when trying to send an email before failing. |
136+
| `EMAIL_USE_TLS` | False | Whether to use TLS to send email with SMTP |
137+
| `SENTRY_DSN` | `None` | To enable error monitoring with Sentry (leave empty to leave it deactivated). |
138+
| `LEGADILO_ARTICLE_FETCH_TIMEOUT` | 50 | The fetch timeout when fetching articles in seconds. |
139+
| `LEGADILO_RSS_FETCH_TIMEOUT` | 300 | The fetch timeout when fetching feeds in seconds. |
140+
| `LEGADILO_CONTACT_EMAIL` | `None` | The contact email to display to authenticated user. |
141+
| `LEGADILO_CUSTOM_SCRIPT` | `None` | To inject an extra script (typically a visitor tracker) to the site. Must a JSON mapping of attribute to value like this: `{"src": "https://plausible.io"}`.<br>Nonce and `defer` will be added automatically. |

Diff for: legadilo/core/context_processors.py

+1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ def provide_global_context(request):
2121
return {
2222
"VERSION": settings.VERSION,
2323
"CONTACT_EMAIL": settings.CONTACT_EMAIL,
24+
"CUSTOM_SCRIPT": settings.CUSTOM_SCRIPT,
2425
}

Diff for: legadilo/feeds/admin.py

+23
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
# You should have received a copy of the GNU Affero General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
import logging
18+
1719
from django.contrib import admin
20+
from django.core.management import call_command
1821

1922
from legadilo.feeds.models import (
2023
Feed,
@@ -25,6 +28,8 @@
2528
FeedUpdate,
2629
)
2730

31+
logger = logging.getLogger(__name__)
32+
2833

2934
class FeedTagInline(admin.TabularInline):
3035
model = FeedTag
@@ -65,6 +70,24 @@ class FeedAdmin(admin.ModelAdmin):
6570
FeedTagInline,
6671
FeedUpdateInline,
6772
]
73+
actions = ["refresh_feeds", "force_refresh_feeds"]
74+
75+
@admin.action(description="Refresh selected feeds")
76+
def refresh_feeds(self, request, queryset):
77+
logger.info("Refresh selected feeds from admin")
78+
call_command("update_feeds", "--feed-ids", *queryset.values_list("id", flat=True))
79+
logger.info(f"Refreshed {len(queryset)} feeds from admin")
80+
81+
@admin.action(description="Force a refresh of selected feeds")
82+
def force_refresh_feeds(self, request, queryset):
83+
logger.info("Force refresh selected feeds from admin")
84+
call_command(
85+
"update_feeds",
86+
"--feed-ids",
87+
*queryset.values_list("id", flat=True),
88+
"--force",
89+
)
90+
logger.info(f"Refreshed {len(queryset)} feeds from admin")
6891

6992

7093
@admin.register(FeedCategory)

Diff for: legadilo/feeds/services/feed_parsing.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
FullSanitizeValidator,
4242
ValidUrlValidator,
4343
default_frozen_model_config,
44+
is_url_valid,
4445
normalize_url,
4546
truncate,
4647
)
@@ -141,7 +142,7 @@ def build_feed_data_from_parsed_feed(parsed_feed: FeedParserDict, resolved_url:
141142

142143
return FeedData(
143144
feed_url=resolved_url,
144-
site_url=_normalize_found_link(parsed_feed.feed.get("link", resolved_url)),
145+
site_url=_get_feed_site_link(parsed_feed.feed.get("link"), resolved_url),
145146
title=feed_title,
146147
description=full_sanitize(parsed_feed.feed.get("description", "")),
147148
feed_type=constants.SupportedFeedType(parsed_feed.version),
@@ -210,6 +211,14 @@ def _find_feed_page_content(page_content: str) -> str:
210211
return feed_urls[0][0]
211212

212213

214+
def _get_feed_site_link(site_link: str | None, feed_link: str) -> str:
215+
if site_link is None or not is_url_valid(site_link):
216+
parsed_feed_url = urlparse(feed_link)
217+
return f"{parsed_feed_url.scheme}://{parsed_feed_url.netloc}"
218+
219+
return _normalize_found_link(site_link)
220+
221+
213222
def _normalize_found_link(link: str):
214223
if link.startswith("//"):
215224
link = f"https:{link}"

Diff for: legadilo/feeds/tests/test_services/test_feed_parsing.py

+36
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
NoFeedUrlFoundError,
2525
_find_feed_page_content,
2626
_find_youtube_rss_feed_link,
27+
_get_feed_site_link,
2728
_parse_articles_in_feed,
2829
get_feed_data,
2930
parse_feed,
@@ -313,3 +314,38 @@ def test_parse_articles(self, feed_content, snapshot):
313314
)
314315

315316
snapshot.assert_match(serialize_for_snapshot(articles), "articles.json")
317+
318+
319+
class TestGetFeedSiteUrl:
320+
@pytest.mark.parametrize(
321+
("found_site_link", "feed_link", "expected_site_link"),
322+
[
323+
pytest.param(
324+
None,
325+
"https://example.com/feed.xml",
326+
"https://example.com",
327+
id="failed-to-find-feed",
328+
),
329+
pytest.param(
330+
"https://example.fr/the-site/",
331+
"https://example.com/feed.xml",
332+
"https://example.fr/the-site/",
333+
id="found-in-full",
334+
),
335+
pytest.param(
336+
"//example.com",
337+
"https://example.com/feed.xml",
338+
"https://example.com",
339+
id="found-without-scheme-full",
340+
),
341+
pytest.param(
342+
"/", "https://example.com/feed.xml", "https://example.com", id="not-full-url"
343+
),
344+
],
345+
)
346+
def test_get_feed_site_url(
347+
self, found_site_link: str | None, feed_link: str, expected_site_link: str
348+
):
349+
build_site_link = _get_feed_site_link(found_site_link, feed_link)
350+
351+
assert build_site_link == expected_site_link

Diff for: legadilo/feeds/views/subscribe_to_feed_view.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
#
1414
# You should have received a copy of the GNU Affero General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16-
1716
import json
17+
import logging
1818
from http import HTTPMethod, HTTPStatus
1919
from typing import cast
2020

@@ -47,6 +47,8 @@
4747
get_feed_data,
4848
)
4949

50+
logger = logging.getLogger(__name__)
51+
5052

5153
class SubscribeToFeedForm(forms.Form):
5254
url = forms.URLField(
@@ -207,7 +209,8 @@ async def _handle_creation(request: AuthenticatedHttpRequest): # noqa: PLR0911
207209
_("The feed file is too big, we won't parse it. Try to find a more lightweight feed."),
208210
)
209211
return HTTPStatus.BAD_REQUEST, form
210-
except (InvalidFeedFileError, PydanticValidationError, ValueError, TypeError):
212+
except (InvalidFeedFileError, PydanticValidationError, ValueError, TypeError) as e:
213+
logger.debug("Failed to parse feed: %s", e)
211214
messages.error(
212215
request,
213216
_(

Diff for: legadilo/static/css/base.css

+8
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ html {
9090
max-height 1s ease-out;
9191
}
9292

93+
.refresh-btn {
94+
right: 0.5rem;
95+
bottom: 0.5rem;
96+
width: max-content;
97+
margin-left: auto;
98+
display: block;
99+
}
100+
93101
@media (width >= 768px) {
94102
.article-card-container {
95103
max-height: 500px;

Diff for: legadilo/templates/base.html

+11-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
<!-- This cannot be deferred: we want the proper theme to apply immediately, not after the page has loaded -->
6363
<script src="{% static 'js/theme_chooser.js' %}"
6464
nonce="{{ request.csp_nonce }}"></script>
65+
{% if CUSTOM_SCRIPT %}
66+
<script defer nonce="{{ request.csp_nonce }}" {% for attr, value in CUSTOM_SCRIPT.items %} {{ attr }}="{{ value }}"{% endfor %}></script>
67+
{% endif %}
6568
{% block page_js %}
6669
{% endblock page_js %}
6770
{% endblock javascript %}
@@ -187,6 +190,9 @@ <h1 class="modal-title fs-5"></h1>
187190
<a class="dropdown-item" href="{% url 'feeds:feed_category_admin' %}">{% translate "Feed categories admin" %}</a>
188191
<a class="dropdown-item" href="{% url 'reading:reading_lists_admin' %}">{% translate "Reading lists admin" %}</a>
189192
<a class="dropdown-item" href="{% url 'users:detail' request.user.pk %}">{% translate "My Profile" %}</a>
193+
{% if request.user.is_staff %}
194+
<a class="dropdown-item" href="{% url 'admin:index' %}">{% translate "Admin" %}</a>
195+
{% endif %}
190196
{# URL provided by django-allauth/account/urls.py #}
191197
<a class="dropdown-item" href="{% url 'account_logout' %}">{% translate "Sign Out" %}</a>
192198
</div>
@@ -232,27 +238,27 @@ <h1 class="modal-title fs-5"></h1>
232238
<p class="ps-4 pe-4 text-center">
233239
{% if VERSION %}
234240
{% blocktranslate with version=VERSION %}
235-
Legadilo version {{ version }}
241+
Legadilo <a class="text-reset" href="https://github.com/Jenselme/legadilo/blob/main/CHANGELOG.md">version {{ version }}</a>
236242
{% endblocktranslate %}
237243
{% else %}
238244
Legadilo
239245
{% endif %}
240246
</p>
241247
<div class="d-flex flex-column flex-md-row justify-content-center align-items-center">
242248
<p class="ps-4 pe-4">
243-
<a class="text-white" href="{% url 'website:home' %}">{% translate "About" %}</a>
249+
<a class="text-reset" href="{% url 'website:home' %}">{% translate "About" %}</a>
244250
</p>
245251
<p class="ps-4 pe-4">
246-
<a class="text-white" href="{% url 'website:privacy' %}">{% translate "Privacy" %}</a>
252+
<a class="text-reset" href="{% url 'website:privacy' %}">{% translate "Privacy" %}</a>
247253
</p>
248254
<p class="ps-4 pe-4">
249-
<a class="text-white" href="https://github.com/Jenselme/legadilo/issues">{% translate "Report a bug." %}</a>
255+
<a class="text-reset" href="https://github.com/Jenselme/legadilo/issues">{% translate "Report a bug." %}</a>
250256
</p>
251257
<p class="ps-4 pe-4">
252258
{% if CONTACT_EMAIL and user.is_authenticated %}
253259
<span>
254260
{% blocktranslate with email=CONTACT_EMAIL %}
255-
Contact us at <a href="mailto:{{ email }}">{{ email }}</a>
261+
Contact us at <a class="text-reset" href="mailto:{{ email }}">{{ email }}</a>
256262
{% endblocktranslate %}
257263
</span>
258264
{% endif %}

Diff for: legadilo/templates/reading/list_of_articles.html

+4
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,8 @@ <h2 class="fs-6 col-6 col-md-2 align-middle d-flex align-items-center px-0 mx-0"
164164
value="{{ displayed_reading_list.id }}" />
165165
<input type="hidden" name="from_url" value="{{ from_url }}" />
166166
</form>
167+
<a class="btn btn-outline-primary sticky-bottom refresh-btn bg-secondary-subtle"
168+
aria-label="{% translate 'Refresh current reading list' %}"
169+
role="button"
170+
href="{{ request.get_full_path }}">{% include "core/partials/bs-icons/refetch.html" %}</a>
167171
{% endblock content %}

Diff for: legadilo/templates/website/privacy.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ <h1 class="display-1">{% translate "Privacy" %}</h1>
2323
<p>
2424
{% blocktranslate %}
2525
We don’t read nor sell your data and <em>never</em> will.
26-
We may add some tracking to have stats about the site usage.
27-
It will <em>be GDPR compatible</em>, and we will collect so little we won’t even need to add a cookie banner.
26+
We track visits with <a href="https://plausible.io">Plausible.io</a>.
27+
It is <em>GDPR compatible</em>, and collects so little we won’t even need to bother you with a cookie banner.
2828
{% endblocktranslate %}
2929
</p>
3030
<p>

0 commit comments

Comments
 (0)