shaarpy-0.1.0
FoxMaSk 1 year ago
parent eacda197ac
commit 21f4ca8713
  1. 112
      .gitignore
  2. 13
      LICENSE
  3. 108
      README.md
  4. 1
      VERSION.txt
  5. BIN
      docs/daily.png
  6. BIN
      docs/new_link.png
  7. BIN
      docs/shaarpy_home.png
  8. BIN
      docs/tags_list.png
  9. 22
      manage.py
  10. 4
      requirements.txt
  11. 44
      setup.cfg
  12. 2
      setup.py
  13. 0
      shaarpy/__init__.py
  14. 16
      shaarpy/asgi.py
  15. 19
      shaarpy/env.sample
  16. 52
      shaarpy/forms.py
  17. 31
      shaarpy/migrations/0001_initial.py
  18. 0
      shaarpy/migrations/__init__.py
  19. 24
      shaarpy/models.py
  20. 140
      shaarpy/settings.py
  21. 77
      shaarpy/templates/base.html
  22. 49
      shaarpy/templates/edit_me.html
  23. 30
      shaarpy/templates/me.html
  24. 36
      shaarpy/templates/registration/login.html
  25. 35
      shaarpy/templates/shaarpy/daily_list.html
  26. 59
      shaarpy/templates/shaarpy/links_detail.html
  27. 59
      shaarpy/templates/shaarpy/links_form.html
  28. 92
      shaarpy/templates/shaarpy/links_list.html
  29. 17
      shaarpy/templates/shaarpy/tags_list.html
  30. 0
      shaarpy/templatetags/__init__.py
  31. 20
      shaarpy/templatetags/shaarpy_extras.py
  32. 44
      shaarpy/urls.py
  33. 301
      shaarpy/views.py
  34. 16
      shaarpy/wsgi.py

112
.gitignore vendored

@ -0,0 +1,112 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
*.secret
.bumpversion.cfg
.bandit
*.sqlite*

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2021 FoxMaSk <foxmask+wtfpl@pm.me>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

@ -1,3 +1,107 @@
# shaarpy
# ShaaPy
Shaarli in Python :P
The very nice Shaarli (https://sebsauvage.net/wiki/doku.php?id=php:shaarli) 'cloned' in Python
Save your notes and links to share ... or not ;)
![Main page](https://framagit.org/foxmask/shaarpy/-/raw/master/shaarpy/docs/shaarpy_home.png)
## Installation
## :package: Installation
### Requirements
* Python from 3.8 to 3.10
* Django 4.0
* pandoc
### Installation
pandoc
```bash
sudo apt install pandoc
```
create a virtualenv
```bash
python3 -m venv shaarpy
cd shaarpy
source bin/activate
```
install the project
```bash
git clone https://framagit.org/foxmask/shaarpy
cd shaarpy
```
## :wrench: Settings
copy the sample config file
```bash
cp env.sample .env
```
and set the following values
```ini
SHAARPY_NAME=Home Sweet Links
SHAARPY_DESCRIPTION=Links, tech links, life links
SECRET=!DONTFORGETTOCHANGETHISVALUE!
DEBUG=True # or False in prod
DB_ENGINE='django.db.backends.sqlite3'
DB_NAME='db.sqlite3'
DB_USER=''
DB_PASSWORD=''
DB_HOST=''
DB_PORT=''
TIME_ZONE='Europe/Paris'
LANGUAGE_CODE='en-en'
USE_I18N=True
USE_L10N=True
USE_TZ=True
SECRET_KEY=!TOBEDEFINED!
```
## :dvd: Database
setup the database
```bash
cd shaarpy
python manage.py createsuperuser
python manage.py migrate
```
## :mega: Running the Server
### start the project
```bash
python manage.py runserver localhost:8001
```
then, access the project with your browser http://127.0.0.1:8001/
## Usage
### Add a new link
![New links](https://framagit.org/foxmask/shaarpy/-/raw/master/shaarpy/docs/new_link.png)
### Tags list
![Tags list](https://framagit.org/foxmask/shaarpy/-/raw/master/shaarpy/docs/tags_list.png)
to easyly find links by tags
### Daily links
![Daily links](https://framagit.org/foxmask/shaarpy/-/raw/master/shaarpy/docs/daily.png)

@ -0,0 +1 @@
0.1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shaarpy.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

@ -0,0 +1,4 @@
Django==4.0.1
django-environ==0.8.1
newspaper3k==0.2.8
pypandoc==1.7.2

@ -0,0 +1,44 @@
[metadata]
name=shaarpy
description="Shaarli in Python"
author=ํญ์Šค๋งˆ์Šคํฌ
author_email=foxmask+git@pm.me
url=https://framagit.org/foxmask/shaarpy
long_description=file: README.md
long_description_content_type=text/markdown
license=WTFPL
keywords=python starlette
version=file: VERSION.txt
classifiers=
Development Status :: 4 - Beta
Environment :: Web Environment
License :: Public Domain
Operating System :: OS Independent
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Internet
Topic :: Communications
Topic :: Database
[options]
zip_safe=false
include_package_data=true
install_requires=
Django==4.0.1
django-environ==0.8.1
newspaper3k==0.2.8
pypandoc==1.7.2
[options.packages.find]
exclude=
tests
[flake8]
max-line-length=120
exclude=.tox,build
ignore = E402, F401

@ -0,0 +1,2 @@
from setuptools import setup
setup()

@ -0,0 +1,16 @@
"""
ASGI config for shaarpy project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shaarpy.settings')
application = get_asgi_application()

@ -0,0 +1,19 @@
SHAARPY_NAME=Home Sweet Links
SHAARPY_DESCRIPTION=Links, tech links, life links
SECRET=!DONTFORGETTOCHANGETHISVALUE!
DEBUG=True
ALLOWED_HOSTS='127.0.0.1,localhost'
DB_ENGINE='django.db.backends.sqlite3'
DB_NAME='db.sqlite3'
DB_USER=''
DB_PASSWORD=''
DB_HOST=''
DB_PORT=''
TIME_ZONE='Europe/Paris'
LANGUAGE_CODE='en-en'
USE_I18N=True
USE_L10N=True
USE_TZ=True

@ -0,0 +1,52 @@
# coding: utf-8
"""
ShaarPy
"""
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.forms import ModelForm, TextInput, Textarea, CheckboxInput, PasswordInput, EmailInput
from shaarpy.models import Links
class LinksForm(ModelForm):
class Meta:
model = Links
fields = ('url', 'title', 'text', 'tags', 'private')
widgets = {
'tags': TextInput(attrs={'class': 'form-control'}),
'url': TextInput(attrs={'class': 'form-control', 'placeholder': _('URL or leave if empty for creating a note')}),
'title': TextInput(attrs={'class': 'form-control', 'placeholder': _('Note:')}),
'text': Textarea(attrs={'class': 'form-control', 'placeholder': _('content')}),
'private': CheckboxInput(attrs={'class': 'form-check-input'}),
}
#class LoginForm(ModelForm):
# """
# Form to manage the login page
# """
# class Meta:
# model = User
# fields = ('username', 'password')
# widgets = {
# 'username': TextInput(attrs={'class': 'form-control', 'placeholder': 'Username'}),
# 'password': PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Password'}),
# }
class MeForm(ModelForm):
"""
form to edit its profile
"""
class Meta:
model = User
fields = ('email', 'last_name')
widgets = {
'last_name': TextInput(attrs={'class': 'form-control', 'placeholder': _('Last name')}),
'email': EmailInput(attrs={'class': 'form-control', 'placeholder': _('Email')}),
}

@ -0,0 +1,31 @@
# Generated by Django 4.0.1 on 2022-01-10 19:59
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tags', models.CharField(blank=True, max_length=255, null=True)),
('url', models.URLField(blank=True, max_length=2048, null=True)),
('title', models.CharField(max_length=255, blank=True, null=True)),
('text', models.TextField(blank=True, null=True)),
('private', models.BooleanField(default=False)),
('date_created', models.DateTimeField(default=datetime.datetime.now)),
],
options={
'verbose_name_plural': 'Links',
'ordering': ['-date_created'],
},
),
]

@ -0,0 +1,24 @@
# coding: utf-8
"""
ShaarPy
"""
import datetime
from django.db import models
from django.urls import reverse
class Links(models.Model):
tags = models.CharField(max_length=255, null=True, blank=True)
url = models.URLField(max_length=2048, null=True, blank=True)
title = models.CharField(max_length=255, null=True, blank=True)
text = models.TextField(null=True, blank=True)
private = models.BooleanField(default=False)
date_created = models.DateTimeField(default=datetime.datetime.now)
class Meta:
verbose_name_plural = "Links"
ordering = ['-date_created']
def get_absolute_url(self):
return reverse('home')

@ -0,0 +1,140 @@
"""
Django settings for shaarpy project.
Generated by 'django-admin startproject' using Django 4.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
# from django.urls import reverse
from pathlib import Path
import environ
import os
env = environ.Env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
env_file_path = os.path.join(BASE_DIR, 'shaarpy', '.env')
env_file = environ.Env.read_env(env_file_path)
env.read_env(env_file)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env.str('SECRET_KEY', default='!DONTFORGETTOCHANGETHISVALUE!')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool('DEBUG', default=False) # set to False when using in production
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost'])
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shaarpy',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'shaarpy.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'shaarpy.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': env.str('DB_ENGINE', default='django.db.backends.sqlite3'),
'NAME': env.str('DB_NAME', default=BASE_DIR / 'db.sqlite3'),
'USER': env.str('DB_USER', default=''),
'PASSWORD': env.str('DB_PASSWORD', default=''),
'HOST': env.str('DB_HOST', default=''),
'PORT': env.str('DB_PORT', default=''),
},
'TEST': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'nyuseu-test.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = env.str('LANGUAGE_CODE', default='en-en')
TIME_ZONE = env.str('TIME_ZONE', default='UTC')
USE_I18N = env.bool('USE_I18N', default=True)
USE_L10N = env.bool('USE_L10N', default=True)
USE_TZ = env.bool('USE_TZ', default=True)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static'
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGOUT_REDIRECT_URL = '/'
SHAARPY_NAME = env.str('SHAARPY_NAME', default="Shaarpy - My Links")
SHAARPY_DESCRIPTION = env.str('SHAARPY_DESCRIPTION')

@ -0,0 +1,77 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ SHAARPY_NAME }}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"/>
<link rel="stylesheet" href="{% static 'shaarpy/css/base.css' %}"/>
<meta name="author" content="FoxMaSk">
</head>
<body role="document">
<nav class="navbar navbar-expand-md navbar-light fixed-top sticky-top" style="background-color: #ebcb8b;">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}" title="Home">
<span class="menu-collapsed">ShaarPy {{ SHAARPY_NAME }}</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link shaarpy-link" href="{% url 'home' %}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link shaarpy-link" href="{% url 'link_create' %}" title="Add a link or note">+Shaarpy</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'tags_list' %}" title="Cloud of tags">Tags Cloud</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'daily' %}" title="Daily links">Daily links</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link shaarpy-link" href="{% url 'atom' %}"><i class="fas fa-rss"></i></a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{% url 'me' %}"><i class="far fa-user"></i></a>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}"><i class="fas fa-sign-out-alt"></i></a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- container -->
{% block content %}
{% endblock %}
<!--/.container-->
</div>
</div>
<hr/>
<footer class="footer">
{% block footer %}
<div class="container-fluid">
<div class="row">
<div class="col-xs-4 col-md-4 col-lg-4">
<p class="fs-5">Since 2022 <a href="https://framagit.org/foxmask/shaarpy" title="Shaarpy project homepage">Shaarpy</a></p>
</div>
</div>
</div>
{% endblock %}
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "My Profile" %} - {{ SHAARPY_NAME }}{% endblock %}
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<h1 class="page-header">{% trans "My Profile" %}</h1>
<form role="form" method="post" class="form-horizontal">{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% if form.errors.items %}You should fix the error(s) below {% endif %}
</div>
{% endif %}
<div class="tab-content">
<table class='table table-hover table-bordered table-striped caption-top'>
<caption>{% trans "Profile Details" %}</caption>
<thead>
<tr>
<th>{% trans "Last name" %}</th>
<th>{% trans "Email" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{{ form.last_name }}
</td>
<td>
{{ form.email }}
</td>
</tr>
{% if form.last_name.errors or form.email.errors %}
<tr>
<td class="alert alert-danger">{% if form.last_name.errors %}{{ form.last_name.errors }}{% endif %}</td>
<td class="alert alert-danger">{% if form.email.errors %}{{ form.email.errors }}{% endif %}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="{% trans 'Save' %}" />
</div>
</form>
</div>
{% endblock %}

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "My Profile" %} - {{ SHAARPY_NAME }}{% endblock %}
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<h2 class="page-header">{% trans "My Profile" %}</h2>
<table class='table table-hover table-bordered table-striped caption-top'>
<caption>{% trans "Profile Details" %}</caption>
<thead>
<tr>
<th>{% trans "Last name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Last connection" %}</th>
<th>{% trans "Created" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ object.last_name }}</td>
<td>{{ object.email }}</td>
<td>{{ object.last_login }}</td>
<td>{{ object.date_joined }}</td>
</tr>
</tbody>
</table>
<a href="{% url 'edit_me' %}"><span class="btn btn-primary"> {% trans "Edit" %}</span></a>
</div>
{% endblock %}

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Log in" %} - {{ SHAARPY_NAME }} {% endblock %}
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<h1 class="page-header">{% trans "Log in" %}</h1>
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<p>{{ form.non_field_errors }}</p>
</div>
{% endif %}
<form role="form" method="post" class="form-horizontal" action="{% url 'login' %}">
{% csrf_token %}
<div class="form-group">
<label class="form-label col-md-2" for="id_username">{% trans 'Login' %}</label>
{{ form.username }}
{% if form.username.errors %}
<p class="help-block">{{ form.username.errors.as_text }}</p>
{% endif %}
</div>
<div class="form-group">
<label class="form-label col-md-2" for="id_password">{% trans 'Password' %}</label>
{{ form.password }}
{% if form.password.errors %}
<p class="help-block">{{ form.password.errors.as_text }}</p>
{% endif %}
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="{% trans 'Log in' %}" />
<input type="hidden" name="next" value="{{ next }}" />
</div>
</form>
</div>
{% endblock %}

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% load i18n %}
{% load shaarpy_extras %}
<title>{% block title %}{{ SHAARPY_NAME }} :: {% trans 'Daily Links' %} {{ tag }}{% endblock %}</title>
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2 mt-3">
<h1>{% trans 'Daily Links' %}</h1>
<h5>list of the links of each day</h5>
<div class="row row-cols-1 row-cols-md-3 g-4">
{% for data in object_list %}
<div class="col">
<div class="card">
<div class="card-body">
{% if data.url %}
<h5><a href="{{ data.url }}" target="_blank">{{ data.title }}</a></h5>
{% else %}
<h5><a href=" url 'link_detail' data.id ">{{ data.title }}</a></h5>
{% endif %}
<p class="card-text">{{ data.text | markdown |safe }}</p>
</div>
<div class="card-footer text-muted">
{% if data.private %}
<i class="fas fa-key"></i> -
{% endif %}
<i class="far fa-clock"></i> {{ data.date_created }}
{% if data.tags %}
- <i class="fas fa-tags"></i> {{ data.tags | tags | safe }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% load i18n %}
{% load shaarpy_extras %}
<title>{% block title %}{{ SHAARPY_NAME }} :: {% trans "View" %} {{ object.title }} {% endblock %}</title>
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<div class="card">
<div class="card-body">
{% if object.url %}
<h5><a href="{{ object.url }}" target="_blank">{{ object.title }}</a></h5>
{% else %}
<h5><a href="{% url 'link_detail' object.id %}">{{ object.title }}</a></h5>
{% endif %}
<p class="card-text">{{ object.text | markdown |safe }}</p>
</div>
<div class="card-footer text-muted">
{% if object.private %}
<i class="fas fa-key"></i> -
{% endif %}
<i class="far fa-clock"></i> {{ object.date_created }}
{% if request.user.is_authenticated == True %}
<a type="button" href="{% url 'link_edit' object.id %}" class="btn btn-outline-primary btn-sm">
<i class="far fa-edit"></i>
</a>
<a type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal{{ object.id }}">
<i class="far fa-trash-alt"></i>
</a> -
<!-- Modal -->
<div class="modal fade" id="deleteModal{{ object.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ object.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ object.id }}">{% trans 'Deletion of your link' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% blocktrans %}
Are you sure you want to remove this link ?
{% endblocktrans %}
<p>
{% if object.link %}{{ object.link }}{% endif %}
{% if object.title %}{{ object.title }}{% endif %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">No</button>
<a href="{% url 'link_delete' object.id %}" type="button" class="btn btn-danger">Yes</a>
</div>
</div>
</div>
</div>
{% endif %}
<a href="{% url 'link_detail' object.id %}">Permalink</a> - {% if object.url %}{{ object.url }}{% else %}{% url 'link_detail' object.id %}{% endif %}
{% if object.tags %}
- <i class="fas fa-tags"></i> {{ object.tags | tags | safe }}
{% endif %}
</div>
</div>
{% endblock %}

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% load i18n %}
<title>{% block title %}{{ SHAARPY_NAME }} :: New shaarpy/note{% endblock %}</title>
{% block content %}
{% if messages %}
<div class="alert alert-warning">
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<form action="" method="post" role="form" class="form-horizontal">
{% csrf_token %}
<fieldset>
{{ form.non_field_errors }}
{% if edit %}
<legend> Update the link/note</legend>
{% else %}
<legend> Share a link/a note</legend>
{% endif %}
<div class="mb-3">
<label class="col-sm-9 control-label" for="id_url"> URL</label>
<div class="col-sm-9">{{ form.url }}</div>
<div class="col-sm-offset-2 col-sm-8">{{ form.url.errors }}</div>
</div>
<div class="mb-3">
<label class="col-sm-9 control-label" for="id_title"> {% trans 'Title' %}</label>
<div class="col-sm-9">{{ form.title }}</div>
<div class="col-sm-offset-2 col-sm-8">{{ form.title.errors }}</div>
</div>
<div class="mb-3">
<label class="col-sm-9 control-label" for="id_text"> {% trans 'Text' %}</label>
<div class="col-sm-9">{{ form.text }}</div>
<div class="col-sm-offset-2 col-sm-8">{{ form.text.errors }}</div>
</div>
<div class="mb-3">
<label class="col-sm-9 control-label" for="id_tags"> {% trans 'Tags' %}</label>
<div class="col-sm-9">{{ form.tags }}</div>
<div class="col-sm-offset-2 col-sm-8">{{ form.tags.errors }}</div>
</div>
<div class="mb-3">
<label class="col-sm-9 control-label" for="id_private"> {% trans 'Private' %}</label>
<div class="col-sm-9">{{ form.private }}</div>
<div class="col-sm-offset-2 col-sm-8">{{ form.private.errors }}</div>
</div>
<a href="{% url 'home' %}" class="btn btn-success"><i class="fas fa-long-arrow-alt-left"></i> {% trans "Back" %}</a>
{% if edit %}
<button class="btn btn-primary">{% trans "Update link" %}</button>
{% else %}
<button class="btn btn-primary"><i class="fas fa-plus"></i> {% trans "Add link" %}</button>
{% endif %}
</fieldset>
</form>
</div>
{% endblock %}

@ -0,0 +1,92 @@
{% extends "base.html" %}
{% load i18n %}
{% load shaarpy_extras %}
{% if tag %}
<title>{% block title %}{{ SHAARPY_NAME }} :: {% trans 'Links for tag' %} {{ tag }}{% endblock %}</title>
{% endif %}
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2">
<nav aria-label="Page navigation" class="mt-3">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">&laquo; first</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">previous</a></li>
{% endif %}
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.</a>
</li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">next</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a></li>
{% endif %}
</ul>
</nav>
{% for data in object_list %}
<div class="card">
<div class="card-body">
{% if data.url %}
<h5><a href="{{ data.url }}" target="_blank">{{ data.title }}</a></h5>
{% else %}
<h5><a href="{% url 'link_detail' data.id %}">{{ data.title }}</a></h5>
{% endif %}
<p class="card-text">{{ data.text | markdown |safe }}</p>
</div>
<div class="card-footer text-muted">
{% if data.private %}
<i class="fas fa-key"></i> -
{% endif %}
<i class="far fa-clock"></i> {{ data.date_created }}
<a type="button" href="{% url 'link_edit' data.id %}" class="btn btn-outline-primary btn-sm">
<i class="far fa-edit"></i>
</a>
<a type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal{{ data.id }}">
<i class="far fa-trash-alt"></i>
</a> -
<!-- Modal -->
<div class="modal fade" id="deleteModal{{ data.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ data.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ data.id }}">{% trans 'Deletion of your link' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% blocktrans %}
Are you sure you want to remove this link ?
{% endblocktrans %}
<p>
{% if data.link %}{{ data.link }}{% endif %}
{% if data.title %}{{ data.title }}{% endif %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">No</button>
<a href="{% url 'link_delete' data.id %}" type="button" class="btn btn-danger">Yes</a>
</div>
</div>
</div>
</div>
<a href="{% url 'link_detail' data.id %}">Permalink</a> - {% if data.url %}{{ data.url }}{% else %}{% url 'link_detail' data.id %}{% endif %}
{% if data.tags %}
- <i class="fas fa-tags"></i> {{ data.tags | tags | safe }}
{% endif %}
</div>
</div>
{% endfor %}
<nav aria-label="Page navigation" class="mt-3">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">&laquo; first</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">previous</a></li>
{% endif %}
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.</a>
</li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">next</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a></li>
{% endif %}
</ul>
</nav>
</div>
{% endblock %}

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% load i18n %}
{% load shaarpy_extras %}
<title>{% block title %}{{ SHAARPY_NAME }} :: {% trans "Tags list" %}{% endblock %}</title>
{% block content %}
<div class="col-xs-8 col-md-8 col-lg-8 offset-xs-2 offset-md-2 offset-lg-2 mt-3">
<h1>{% trans "Tags list" %}</h1>
{% for key, value in tags.items %}
<a href="{% url 'links_by_tag_list' key %}" type="button" class="p-3 m-2 btn btn-dark position-relative">
{{ key }}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ value }}
</span>
</a>
{% endfor %}
</div>
{% endblock %}

@ -0,0 +1,20 @@
from django import template
import pypandoc
register = template.Library()
@register.filter(name='tags')
def tags(value):
out = ''
for tag in value.split(','):
out += f'<a href="/links/{tag}"><span class="badge rounded-pill bg-secondary">{tag}</span></a> '
return out
@register.filter(name='markdown')
def makrdown(text):
return pypandoc.convert_text(text, "html", format="md")

@ -0,0 +1,44 @@
"""shaarpy URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path, re_path, include
from shaarpy.views import (HomeView, LinksCreate, LinksDetail, LinksUpdate, link_delete, TagsList, LinksByTagList)
from shaarpy.views import (DailyLinks, LatestLinksFeed, me, MeUpdate)
from shaarpy import settings
urlpatterns = [
# MANAGE USERS
path('accounts/login/', auth_views.LoginView.as_view(extra_context={'SHAARPY_NAME': settings.SHAARPY_NAME}), name="login"),
path('accounts/profile/', me, name="me"),
path('accounts/profile/edit/', MeUpdate.as_view(), name='edit_me'),
path('accounts/', include('django.contrib.auth.urls')),
# THE APP
path('', HomeView.as_view(), name="home"),
path('new/', LinksCreate.as_view(), name='link_create'),
path('edit/<int:pk>/', LinksUpdate.as_view(), name='link_edit'),
path('view/<int:pk>/', LinksDetail.as_view(), name='link_detail'),
path('delete/<int:pk>/', link_delete, name='link_delete'),
path('tags/', TagsList.as_view(), name='tags_list'),
re_path('links/(?P<tags>\w+)$', LinksByTagList.as_view(), name='links_by_tag_list'),
path('daily/', DailyLinks.as_view(), name='daily'),
re_path('daily/(?P<yesterday>\d\d\d\d-\d\d-\d\d)', DailyLinks.as_view(), name='daily'),
# FEEDS
path('feed/', LatestLinksFeed(), name='atom'),
# ADMIN
path('admin/', admin.site.urls),
]

@ -0,0 +1,301 @@
# coding: utf-8
"""
ShaarPy
"""
from datetime import date
from datetime import timedelta
from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.syndication.views import Feed
from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy
from django.utils.text import Truncator
from django.views.generic import ListView, CreateView, UpdateView, DetailView
from newspaper import Article
import newspaper
import pypandoc
from shaarpy.forms import LinksForm, MeForm
from shaarpy.models import Links
from shaarpy import settings
from urllib.parse import urlparse
# Beginning of Handling content of Article with NewsPaPer
def get_host(url):
o = urlparse(url)
return o.scheme + '://' + o.hostname
def get_brand(url):
brand = newspaper.build(get_host(url))
brand.download()
brand.parse()
return brand.brand
def grab_full_article(url):
"""
get the complete article page from the URL
"""
# get the complete article
r = Article(url, keep_article_html=True)
r.download()
r.parse()
# convert into markdown
output = Truncator(r.article_html).chars("400", html=False)
text = pypandoc.convert_text(output, 'md', format='html')
title = r.title + ' - ' + get_brand(url)
return title, text
# End of Handling content of Article with NewsPaPer
@login_required
def link_delete(request, pk):
link = Links.objects.get(pk=pk)
link.delete()
return redirect('home')
class SettingsMixin:
"""
mixin to add settings data to the templates
"""
def get_context_data(self, *, object_list=None, **kwargs):
# get only the unread articles of the folders
context = super(SettingsMixin, self).get_context_data(**kwargs)
context['SHAARPY_NAME'] = settings.SHAARPY_NAME
context['SHAARPY_DESCRIPTION'] = settings.SHAARPY_DESCRIPTION
return context
class HomeView(SettingsMixin, ListView):
"""
Links List
"""
queryset = Links.objects.none()
paginate_by = 10
ordering = ['-date_created']
def get_queryset(self):
if self.request.user.is_authenticated:
queryset = Links.objects.all()
else:
queryset = Links.objects.filter(private=False)
return queryset
def get_context_data(self, *, object_list=None, **kwargs):
queryset = object_list if object_list is not None else self.object_list
page_size = self.paginate_by
context_object_name = self.get_context_object_name(queryset)
context = super(HomeView, self).get_context_data(**kwargs)
paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
context['paginator'] = paginator
context['page_obj'] = page
context['is_paginated'] = is_paginated
context['object_list'] = queryset
if context_object_name is not None:
context[context_object_name] = queryset
context.update(kwargs)
return context
class LinksCreate(SettingsMixin, LoginRequiredMixin, CreateView):
"""
Create Links
"""
model = Links
form_class = LinksForm
def form_valid(self, form):
url = form.cleaned_data['url']
if url is None:
self.object = form.save()
title = form.cleaned_data['title']
text = form.cleaned_data['text']
# just a note
if url == '' and title == '' and text != '':
self.object.title = "Note:"
else:
try:
links = Links.objects.get(url=url)
return redirect('link_detail', **{'pk': links.id})
except Links.DoesNotExist:
self.object = form.save()
title = form.cleaned_data['title']
text = form.cleaned_data['text']
# just a note
if url == '' and title == '' and text != '':
self.object.title = "Note:"
# just an url
elif url != '':
self.object.title, self.object.text = grab_full_article(url)
return super().form_valid(form)
class LinksDetail(SettingsMixin, DetailView):
"""
Link Detail
"""
model = Links
class LinksUpdate(SettingsMixin, LoginRequiredMixin, UpdateView):
"""
Link Update
"""
model = Links
form_class = LinksForm
def get_context_data(self, **kwargs):
context = {}
if self.object:
context['object'] = self.object
context_object_name = self.get_context_object_name(self.object)
if context_object_name:
context[context_object_name] = self.object
context['edit'] = True
context.update(kwargs)
return super().get_context_data(**context)
class LinksByTagList(SettingsMixin, ListView):
"""
LinksByTag List
"""
queryset = Links.objects.none()
paginate_by = 10
def get_queryset(self):
tags = self.kwargs['tags']
if self.request.user.is_authenticated:
queryset = Links.objects.filter(tags__contains=tags)
else:
queryset = Links.objects.filter(tags__contains=tags, private=False)
return queryset
def get_context_data(self, **kwargs):
context = super(LinksByTagList, self).get_context_data(**kwargs)
context['tag'] = self.kwargs['tags']
context.update(kwargs