Compare commits

...

9 Commits

Author SHA1 Message Date
Sascha Ißbrücker
cb7abbfacb Bump version 2021-01-09 22:17:32 +01:00
Sascha Ißbrücker
b844293342 Add favicon (#60)
Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
2021-01-09 00:24:06 +01:00
Sascha Ißbrücker
0f231bcd9f Setup CI for tests 2021-01-02 11:50:16 +01:00
Sascha Ißbrücker
9df270557f Make tag search and assignment case insensitive (#56)
* Make tag assignment and search case-insensitive (#45)

* Add tests for tag case-sensitivity and deduplication (#45)

Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
2021-01-02 11:30:20 +01:00
Sascha Ißbrücker
f98c89e99d Update CHANGELOG.md 2021-01-01 13:25:11 +01:00
Sascha Ißbrücker
6addee1377 Bump version 2021-01-01 13:22:28 +01:00
Sascha Ißbrücker
16ba7f390d Improve README structure 2021-01-01 13:17:47 +01:00
ScientiaSitPotentia
64914fb0d5 Docker compose support (#54)
* added docker-compose files

* updated readme with docker-compose instructions

* updated default docker-compose data folder
2021-01-01 13:11:22 +01:00
Sascha Ißbrücker
ac0f0a7831 Update CHANGELOG.md 2020-12-31 10:02:47 +01:00
18 changed files with 163 additions and 13 deletions

3
.env.sample Normal file
View File

@@ -0,0 +1,3 @@
LD_CONTAINER_NAME=linkding
LD_HOST_PORT=9090
LD_HOST_DATA_DIR=./data

18
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: linkding CI
on: [push]
jobs:
run_tests:
name: Run Django Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: python manage.py test

View File

@@ -1,8 +1,16 @@
# Changelog
## v1.0.0 (31/12/2020)
## v1.1.1 (01/01/2021)
- [**enhancement**] Add docker-compose support [#54](https://github.com/sissbruecker/linkding/pull/54)
---
## v1.1.0 (31/12/2020)
- [**enhancement**] Search autocomplete [#52](https://github.com/sissbruecker/linkding/issues/52)
- [**enhancement**] Improve Netscape bookmarks file parsing [#50](https://github.com/sissbruecker/linkding/issues/50)
---
## v1.0.0 (31/12/2020)
- [**bug**] Import does not import bookmark descriptions [#47](https://github.com/sissbruecker/linkding/issues/47)
- [**enhancement**] Enhancement: return to same page we were on after editing a bookmark [#26](https://github.com/sissbruecker/linkding/issues/26)
- [**bug**] Increase limit on bookmark URL length [#25](https://github.com/sissbruecker/linkding/issues/25)

View File

@@ -28,7 +28,7 @@ docker run --name linkding -p 9090:9090 -d sissbruecker/linkding:latest
By default the application runs on port `9090`, but you can map it to a different host port by modifying the command above.
However for **production use** you also want to mount a data folder on your system, so that the applications database is not stored in the container, but on your hosts file system. This is safer in case something happens to the container and makes it easier to update the container later on, or to run backups. To do so you can use the following extended command, where you replace `{host-data-folder}` with the absolute path to a folder on your system where you want to store the data:
```
```shell
docker run --name linkding -p 9090:9090 -v {host-data-folder}:/etc/linkding/data -d sissbruecker/linkding:latest
```
@@ -40,12 +40,27 @@ If you are using a Linux system you can use the following [shell script](https:/
The script can be configured using using shell variables - for more details have a look at the script itself.
### Docker-compose setup
To install linkding using docker-compose you can use the `docker-compose.yml` file. Copy the `.env.sample` file to `.env` and set your parameters, then run:
```shell
docker-compose up -d
```
### User setup
Finally you need to create a user so that you can access the frontend. Replace the credentials in the following command and run it:
```
**Docker**
```shell
docker exec -it linkding python manage.py createsuperuser --username=joe --email=joe@example.com
```
**Docker-compose**
```shell
docker-compose exec linkding python manage.py createsuperuser --username=joe --email=joe@example.com
```
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.
### Manual setup

BIN
assets/logo.afdesign Normal file

Binary file not shown.

View File

@@ -4,6 +4,8 @@ from django import forms
from django.contrib.auth import get_user_model
from django.db import models
from bookmarks.utils import unique
class Tag(models.Model):
name = models.CharField(max_length=64)
@@ -18,7 +20,8 @@ def parse_tag_string(tag_string: str, delimiter: str = ','):
if not tag_string:
return []
names = tag_string.strip().split(delimiter)
names = [name for name in names if name]
names = [name.strip() for name in names if name]
names = unique(names, str.lower)
names.sort(key=str.lower)
return names

View File

@@ -2,6 +2,7 @@ from django.contrib.auth.models import User
from django.db.models import Q, Count, Aggregate, CharField, Value, BooleanField
from bookmarks.models import Bookmark, Tag
from bookmarks.utils import unique
class Concat(Aggregate):
@@ -41,7 +42,7 @@ def query_bookmarks(user: User, query_string: str):
for tag_name in query['tag_names']:
query_set = query_set.filter(
tags__name=tag_name
tags__name__iexact=tag_name
)
# Sort by modification date
@@ -74,7 +75,7 @@ def query_tags(user: User, query_string: str):
for tag_name in query['tag_names']:
query_set = query_set.filter(
bookmark__tags__name=tag_name
bookmark__tags__name__iexact=tag_name
)
return query_set.distinct()
@@ -95,6 +96,7 @@ def _parse_query_string(query_string):
search_terms = [word for word in keywords if word[0] != '#']
tag_names = [word[1:] for word in keywords if word[0] == '#']
tag_names = unique(tag_names, str.lower)
return {
'search_terms': search_terms,

View File

@@ -1,18 +1,21 @@
import operator
from typing import List
from django.contrib.auth.models import User
from django.utils import timezone
from bookmarks.models import Tag
from bookmarks.utils import unique
def get_or_create_tags(tag_names: List[str], user: User):
return [get_or_create_tag(tag_name, user) for tag_name in tag_names]
tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names]
return unique(tags, operator.attrgetter('id'))
def get_or_create_tag(name: str, user: User):
try:
return Tag.objects.get(name=name, owner=user)
return Tag.objects.get(name__iexact=name, owner=user)
except Tag.DoesNotExist:
tag = Tag(name=name, owner=user)
tag.date_added = timezone.now()

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -5,6 +5,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="{% static 'favicon.png' %}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="Self-hosted bookmark service">
<meta name="robots" content="index,follow">

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@@ -0,0 +1,27 @@
from django.test import TestCase
from bookmarks.models import parse_tag_string
class TagTestCase(TestCase):
def test_parse_tag_string_returns_list_of_tag_names(self):
self.assertCountEqual(parse_tag_string('book, movie, album'), ['book', 'movie', 'album'])
def test_parse_tag_string_respects_separator(self):
self.assertCountEqual(parse_tag_string('book movie album', ' '), ['book', 'movie', 'album'])
def test_parse_tag_string_orders_tag_names_alphabetically(self):
self.assertListEqual(parse_tag_string('book,movie,album'), ['album', 'book', 'movie'])
self.assertListEqual(parse_tag_string('Book,movie,album'), ['album', 'Book', 'movie'])
def test_parse_tag_string_handles_whitespace(self):
self.assertCountEqual(parse_tag_string('\t book, movie \t, album, \n\r'), ['album', 'book', 'movie'])
def test_parse_tag_string_handles_invalid_input(self):
self.assertListEqual(parse_tag_string(None), [])
self.assertListEqual(parse_tag_string(''), [])
def test_parse_tag_string_deduplicates_tag_names(self):
self.assertEqual(len(parse_tag_string('book,book,Book,BOOK')), 1)

View File

@@ -0,0 +1,60 @@
import datetime
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
from bookmarks.models import Tag
from bookmarks.services.tags import get_or_create_tag, get_or_create_tags
User = get_user_model()
class TagTestCase(TestCase):
def setUp(self) -> None:
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123')
def test_get_or_create_tag_should_create_new_tag(self):
get_or_create_tag('Book', self.user)
tags = Tag.objects.all()
self.assertEqual(len(tags), 1)
self.assertEqual(tags[0].name, 'Book')
self.assertEqual(tags[0].owner, self.user)
self.assertTrue(abs(tags[0].date_added - timezone.now()) < datetime.timedelta(seconds=10))
def test_get_or_create_tag_should_return_existing_tag(self):
first_tag = get_or_create_tag('Book', self.user)
second_tag = get_or_create_tag('Book', self.user)
tags = Tag.objects.all()
self.assertEqual(len(tags), 1)
self.assertEqual(first_tag.id, second_tag.id)
def test_get_or_create_tag_should_ignore_casing_when_looking_for_existing_tag(self):
first_tag = get_or_create_tag('Book', self.user)
second_tag = get_or_create_tag('book', self.user)
tags = Tag.objects.all()
self.assertEqual(len(tags), 1)
self.assertEqual(first_tag.id, second_tag.id)
def test_get_or_create_tags_should_return_tags(self):
books_tag = get_or_create_tag('Book', self.user)
movies_tag = get_or_create_tag('Movie', self.user)
tags = get_or_create_tags(['book', 'movie'], self.user)
self.assertEqual(len(tags), 2)
self.assertListEqual(tags, [books_tag, movies_tag])
def test_get_or_create_tags_should_deduplicate_tags(self):
books_tag = get_or_create_tag('Book', self.user)
tags = get_or_create_tags(['book', 'Book', 'BOOK'], self.user)
self.assertEqual(len(tags), 1)
self.assertListEqual(tags, [books_tag])

2
bookmarks/utils.py Normal file
View File

@@ -0,0 +1,2 @@
def unique(elements, key):
return list({key(element): element for element in elements}.values())

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
services:
linkding:
container_name: "${LD_CONTAINER_NAME:-linkding}"
image: sissbruecker/linkding:latest
ports:
- "${LD_HOST_PORT:-9090}:9090"
volumes:
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
restart: unless-stopped

View File

@@ -1,6 +1,6 @@
{
"name": "linkding",
"version": "1.1.0",
"version": "1.2.0",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -1 +1 @@
1.1.0
1.2.0