mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-28 21:06:39 +02:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6775633be5 | ||
![]() |
150dfecc6f | ||
![]() |
81ae55bc1c | ||
![]() |
935189ecc2 | ||
![]() |
7997f20d89 | ||
![]() |
ae27500cde | ||
![]() |
71d853999e | ||
![]() |
70288d6865 |
24
README.md
24
README.md
@@ -9,11 +9,11 @@
|
||||
## Overview
|
||||
- [Introduction](#introduction)
|
||||
- [Installation](#installation)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Docker Compose](#using-docker-compose)
|
||||
- [User Setup](#user-setup)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Managed Hosting Options](#managed-hosting-options)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Docker Compose](#using-docker-compose)
|
||||
- [User Setup](#user-setup)
|
||||
- [Reverse Proxy Setup](#reverse-proxy-setup)
|
||||
- [Managed Hosting Options](#managed-hosting-options)
|
||||
- [Documentation](#documentation)
|
||||
- [Browser Extension](#browser-extension)
|
||||
- [Community](#community)
|
||||
@@ -103,7 +103,7 @@ docker-compose up -d
|
||||
|
||||
To complete the setup, you still have to [create an initial user](#user-setup), so that you can access your installation.
|
||||
|
||||
### User setup
|
||||
### User Setup
|
||||
|
||||
For security reasons, the linkding Docker image does not provide an initial user, so you have to create one after setting up an installation. To do so, replace the credentials in the following command and run it:
|
||||
|
||||
@@ -119,7 +119,7 @@ docker-compose exec linkding python manage.py createsuperuser --username=joe --e
|
||||
|
||||
The command will prompt you for a secure password. After the command has completed you can start using the application by logging into the UI with your credentials.
|
||||
|
||||
Alternatively you can automatically create an initial superuser on startup using the [`LD_SUPERUSER_NAME` option](docs/Options.md#LD_SUPERUSER_NAME).
|
||||
Alternatively you can automatically create an initial superuser on startup using the [`LD_SUPERUSER_NAME` option](docs/Options.md#LD_SUPERUSER_NAME).
|
||||
|
||||
### Reverse Proxy Setup
|
||||
|
||||
@@ -182,6 +182,7 @@ Self-hosting web applications still requires a lot of technical know-how and com
|
||||
|
||||
- [linkding on fly.io](https://github.com/fspoettel/linkding-on-fly) - Guide for hosting a linkding installation on [fly.io](https://fly.io). By [fspoettel](https://github.com/fspoettel)
|
||||
- [PikaPods.com](https://www.pikapods.com/) - Managed hosting for linkding, EU and US regions available. [1-click setup link](https://www.pikapods.com/pods?run=linkding) ([Disclosure](#pikapods))
|
||||
- [CapRover](https://caprover.com/) - Linkding is included as a default one-click app
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -210,7 +211,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
|
||||
- [feed2linkding](https://codeberg.org/strubbl/feed2linkding) A commandline utility to add all web feed item links to linkding via API call. By [Strubbl](https://github.com/Strubbl)
|
||||
- [Helm Chart](https://charts.pascaliske.dev/charts/linkding/) Helm Chart for deploying linkding inside a Kubernetes cluster. By [pascaliske](https://github.com/pascaliske)
|
||||
- [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
|
||||
- [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
|
||||
- [Linka!](https://github.com/cmsax/linka) Web app (also a PWA) for quickly searching & opening bookmarks in linkding, support multi keywords, exclude mode and other advance options. By [cmsax](https://github.com/cmsax)
|
||||
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
|
||||
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
|
||||
@@ -223,7 +224,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
|
||||
### PikaPods
|
||||
|
||||
[PikaPods](https://www.pikapods.com/) has a revenue sharing agreement with this project, sharing some of their revenue from hosting linkding instances. I do not intend to profit from this project financially, so I am in turn donating that revenue. Big thanks to PikaPods for making this possible.
|
||||
[PikaPods](https://www.pikapods.com/) has a revenue sharing agreement with this project, sharing some of their revenue from hosting linkding instances. I do not intend to profit from this project financially, so I am in turn donating that revenue. Big thanks to PikaPods for making this possible.
|
||||
|
||||
See the table below for a list of donations.
|
||||
|
||||
@@ -299,3 +300,8 @@ Start the Django development server with:
|
||||
python3 manage.py runserver
|
||||
```
|
||||
The frontend is now available under http://localhost:8000
|
||||
|
||||
Run all tests with pytest
|
||||
```
|
||||
pytest
|
||||
```
|
||||
|
@@ -59,10 +59,18 @@ class BookmarkItem {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
|
||||
// Toggle notes
|
||||
const notesToggle = element.querySelector(".toggle-notes");
|
||||
if (notesToggle) {
|
||||
notesToggle.addEventListener("click", this.onToggleNotes.bind(this));
|
||||
}
|
||||
|
||||
// Add tooltip to title if it is truncated
|
||||
const titleAnchor = element.querySelector(".title > a");
|
||||
const titleSpan = titleAnchor.querySelector("span");
|
||||
if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {
|
||||
titleAnchor.dataset.tooltip = titleSpan.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
onToggleNotes(event) {
|
||||
|
@@ -22,7 +22,7 @@
|
||||
async function init() {
|
||||
// For now we cache all tags on load as the template did before
|
||||
try {
|
||||
tags = await apiClient.getTags({limit: 1000, offset: 0});
|
||||
tags = await apiClient.getTags({limit: 5000, offset: 0});
|
||||
tags.sort((left, right) => left.name.toLowerCase().localeCompare(right.name.toLowerCase()))
|
||||
} catch (e) {
|
||||
console.warn('TagAutocomplete: Error loading tag list');
|
||||
|
@@ -67,9 +67,9 @@ def archive_bookmark(bookmark: Bookmark):
|
||||
|
||||
def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(is_archived=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=True,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def unarchive_bookmark(bookmark: Bookmark):
|
||||
@@ -81,70 +81,76 @@ def unarchive_bookmark(bookmark: Bookmark):
|
||||
|
||||
def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(is_archived=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=False,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.delete()
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).delete()
|
||||
|
||||
|
||||
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
owned_bookmark_ids = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).values_list('id',
|
||||
flat=True)
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark.tags.add(*tags)
|
||||
bookmark.date_modified = timezone.now()
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
relationships = []
|
||||
for tag in tags:
|
||||
for bookmark_id in owned_bookmark_ids:
|
||||
relationships.append(BookmarkToTagRelationShip(bookmark_id=bookmark_id, tag=tag))
|
||||
|
||||
Bookmark.objects.bulk_update(bookmarks, ['date_modified'])
|
||||
# Insert all bookmark -> tag associations at once, should ignore errors if association already exists
|
||||
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
|
||||
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(date_modified=timezone.now())
|
||||
|
||||
|
||||
def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
owned_bookmark_ids = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).values_list('id',
|
||||
flat=True)
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
bookmark.tags.remove(*tags)
|
||||
bookmark.date_modified = timezone.now()
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
for tag in tags:
|
||||
# Remove all bookmark -> tag associations for the owned bookmarks and the current tag
|
||||
BookmarkToTagRelationShip.objects.filter(bookmark_id__in=owned_bookmark_ids, tag=tag).delete()
|
||||
|
||||
Bookmark.objects.bulk_update(bookmarks, ['date_modified'])
|
||||
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(date_modified=timezone.now())
|
||||
|
||||
|
||||
def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(unread=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=False,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(unread=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=True,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(shared=True, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=True,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
|
||||
bookmarks.update(shared=False, date_modified=timezone.now())
|
||||
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=False,
|
||||
date_modified=timezone.now())
|
||||
|
||||
|
||||
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||
|
@@ -41,8 +41,13 @@ def load_website_metadata(url: str):
|
||||
|
||||
title = soup.title.string.strip() if soup.title is not None else None
|
||||
description_tag = soup.find('meta', attrs={'name': 'description'})
|
||||
description = description = description_tag['content'].strip() if description_tag and description_tag[
|
||||
description = description_tag['content'].strip() if description_tag and description_tag[
|
||||
'content'] else None
|
||||
|
||||
if not description:
|
||||
description_tag = soup.find('meta', attrs={'property': 'og:description'})
|
||||
description = description_tag['content'].strip() if description_tag and description_tag['content'] else None
|
||||
|
||||
end = timezone.now()
|
||||
logger.debug(f'Parsing duration: {end - start}')
|
||||
finally:
|
||||
|
@@ -107,6 +107,18 @@ ul.bookmark-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bookmarks */
|
||||
li[ld-bookmark-item] {
|
||||
position: relative;
|
||||
@@ -122,6 +134,27 @@ li[ld-bookmark-item] {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&[data-tooltip]:hover::after, &[data-tooltip]:focus::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
background-color: #292f62;
|
||||
color: #fff;
|
||||
padding: $unit-1;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid #424a8c;
|
||||
font-size: $font-size-sm;
|
||||
font-style: normal;
|
||||
white-space: normal;
|
||||
animation: 0.3s ease 0s appear;
|
||||
}
|
||||
}
|
||||
|
||||
&.unread .title a {
|
||||
|
@@ -14,11 +14,11 @@
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<div class="title">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener" >
|
||||
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||
<img src="{% static bookmark_item.favicon_file %}" alt="">
|
||||
{% endif %}
|
||||
{{ bookmark_item.title }}
|
||||
<span>{{ bookmark_item.title }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if bookmark_list.show_url %}
|
||||
|
@@ -99,7 +99,7 @@
|
||||
Machine</a>.
|
||||
This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in
|
||||
case it goes offline or its content is modified.
|
||||
Please consider donating to the <a href="https://archive.org/donate/index.php" target="_blank"
|
||||
Please consider donating to the <a href="https://archive.org/donate" target="_blank"
|
||||
rel="noopener">Internet Archive</a> if you make use of this feature.
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -183,9 +183,8 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
</div>
|
||||
''', html)
|
||||
|
||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
|
||||
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
|
||||
self.assertContains(response, '<details class="notes">', count=1)
|
||||
self.assertContains(response, '<details class="notes">', count=1)
|
||||
|
@@ -24,7 +24,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
target="{link_target}"
|
||||
rel="noopener">
|
||||
{favicon_img}
|
||||
{bookmark.resolved_title}
|
||||
<span>{bookmark.resolved_title}</span>
|
||||
</a>
|
||||
''',
|
||||
html
|
||||
|
@@ -336,6 +336,28 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
|
||||
def test_tag_bookmarks_should_handle_existing_relationships(self):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[tag1])
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1])
|
||||
|
||||
BookmarkToTagRelationShip = Bookmark.tags.through
|
||||
self.assertEqual(3, BookmarkToTagRelationShip.objects.count())
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
bookmark3.refresh_from_db()
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||
self.assertEqual(6, BookmarkToTagRelationShip.objects.count())
|
||||
|
||||
def test_tag_bookmarks_should_only_tag_specified_bookmarks(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
|
@@ -29,14 +29,17 @@ class WebsiteLoaderTestCase(TestCase):
|
||||
# clear cached metadata before test run
|
||||
website_loader.load_website_metadata.cache_clear()
|
||||
|
||||
def render_html_document(self, title, description):
|
||||
def render_html_document(self, title, description='', og_description=''):
|
||||
meta_description = f'<meta name="description" content="{description}">' if description else ''
|
||||
meta_og_description = f'<meta property="og:description" content="{og_description}">' if og_description else ''
|
||||
return f'''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title}</title>
|
||||
<meta name="description" content="{description}">
|
||||
{meta_description}
|
||||
{meta_og_description}
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -94,3 +97,19 @@ class WebsiteLoaderTestCase(TestCase):
|
||||
metadata = website_loader.load_website_metadata('https://example.com')
|
||||
self.assertEqual('test title', metadata.title)
|
||||
self.assertEqual('test description', metadata.description)
|
||||
|
||||
def test_load_website_metadata_using_og_description(self):
|
||||
with mock.patch('bookmarks.services.website_loader.load_page') as mock_load_page:
|
||||
mock_load_page.return_value = self.render_html_document('test title', '',
|
||||
og_description='test og description')
|
||||
metadata = website_loader.load_website_metadata('https://example.com')
|
||||
self.assertEqual('test title', metadata.title)
|
||||
self.assertEqual('test og description', metadata.description)
|
||||
|
||||
def test_load_website_metadata_prefers_description_over_og_description(self):
|
||||
with mock.patch('bookmarks.services.website_loader.load_page') as mock_load_page:
|
||||
mock_load_page.return_value = self.render_html_document('test title', 'test description',
|
||||
og_description='test og description')
|
||||
metadata = website_loader.load_website_metadata('https://example.com')
|
||||
self.assertEqual('test title', metadata.title)
|
||||
self.assertEqual('test description', metadata.description)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.23.1",
|
||||
"version": "1.24.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
4
pytest.ini
Normal file
4
pytest.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = siteroot.settings.dev
|
||||
# -- recommended but optional:
|
||||
python_files = tests.py test_*.py *_tests.py
|
@@ -1,13 +1,12 @@
|
||||
asgiref==3.5.2
|
||||
beautifulsoup4==4.11.1
|
||||
bleach==6.0.0
|
||||
bleach-allowlist==1.0.3
|
||||
bleach==6.0.0
|
||||
certifi==2023.7.22
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
confusable-homoglyphs==3.2.0
|
||||
coverage==5.5
|
||||
Django==4.1.13
|
||||
django-appconf==1.0.5
|
||||
django-compressor==4.1
|
||||
django-debug-toolbar==3.6.0
|
||||
@@ -15,15 +14,18 @@ django-generate-secret-key==1.0.2
|
||||
django-registration==3.3
|
||||
django-sass-processor==1.2.1
|
||||
django-widget-tweaks==1.4.12
|
||||
Django==4.1.13
|
||||
django4-background-tasks==1.2.7
|
||||
djangorestframework==3.13.1
|
||||
greenlet==2.0.1
|
||||
greenlet==3.0.1
|
||||
idna==3.3
|
||||
libsass==0.21.0
|
||||
Markdown==3.4.3
|
||||
playwright==1.29.1
|
||||
playwright==1.40.0
|
||||
psycopg2-binary==2.9.5
|
||||
pyee==9.0.4
|
||||
pyee==11.0.1
|
||||
pytest-django==4.7.0
|
||||
pytest==7.4.4
|
||||
python-dateutil==2.8.2
|
||||
pytz==2022.2.1
|
||||
rcssmin==1.1.0
|
||||
|
@@ -1 +1 @@
|
||||
1.23.1
|
||||
1.24.0
|
||||
|
Reference in New Issue
Block a user