@ -0,0 +1,18 @@ | |||||
### Python ### | |||||
# Byte-compiled / optimized / DLL files | |||||
__pycache__/ | |||||
*.py[cod] | |||||
*$py.class | |||||
# Environments | |||||
.env | |||||
.venv | |||||
env/ | |||||
venv/ | |||||
ENV/ | |||||
env.bak/ | |||||
venv.bak/ | |||||
.envrc | |||||
.DS_Store | |||||
.vscode/ |
@ -0,0 +1,13 @@ | |||||
FROM python:3.7 | |||||
LABEL maintainer="Dan Muckerman <danielmuckerman@me.com>" | |||||
WORKDIR /project | |||||
ADD . /project | |||||
RUN rm /project/.envrc | |||||
RUN rm -rf /project/env | |||||
RUN apt-get update && apt-get install -y python3-dev libldap2-dev libsasl2-dev libssl-dev | |||||
RUN pip install -r requirements.txt | |||||
CMD ["flask","run","--host=0.0.0.0"] |
@ -0,0 +1,218 @@ | |||||
from utils import clean_articles | |||||
import ldap as l | |||||
from ldap3 import Server, Connection, ALL, MODIFY_REPLACE | |||||
from flask import Flask, g, request, session, redirect, url_for, render_template | |||||
from flask_simpleldap import LDAP | |||||
from flask_bootstrap import Bootstrap | |||||
from readability import Document | |||||
from readabilipy import simple_json_from_html_string | |||||
import os | |||||
import sqlite3 | |||||
import requests | |||||
from requests.api import head | |||||
from utils import clean_articles | |||||
app = Flask(__name__) | |||||
Bootstrap(app) | |||||
app.secret_key = 'asdf' | |||||
app.debug = True | |||||
# Base | |||||
app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' | |||||
app.config['LDAP_HOST'] = os.environ.get('LDAP_HOST') | |||||
app.config['LDAP_BASE_DN'] = os.environ.get('LDAP_BASE_DN') | |||||
app.config['LDAP_USERNAME'] = os.environ.get('LDAP_USERNAME') | |||||
app.config['LDAP_PASSWORD'] = os.environ.get('LDAP_PASSWORD') | |||||
# OpenLDAP | |||||
app.config['LDAP_OBJECTS_DN'] = 'dn' | |||||
app.config['LDAP_OPENLDAP'] = True | |||||
app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=posixAccount)(uid=%s))' | |||||
short_domain = os.environ.get('SHORT_DOMAIN') | |||||
ldap = LDAP(app) | |||||
server = Server(app.config['LDAP_HOST']) | |||||
conn = Connection(server, app.config['LDAP_USERNAME'], app.config['LDAP_PASSWORD'], auto_bind=True) | |||||
@app.before_request | |||||
def before_request(): | |||||
g.user = None | |||||
if 'user_id' in session: | |||||
# This is where you'd query your database to get the user info. | |||||
g.user = {} | |||||
@app.route('/') | |||||
@ldap.login_required | |||||
def index(): | |||||
user_dict = ldap.get_object_details(session['user_id']) | |||||
if 'user_id' in session: | |||||
user = {'dn': 'cn={},cn=usergroup,ou=users,dc=technicalincompetence,dc=club'.format(user_dict['cn'][0].decode('ascii')), | |||||
'firstName': user_dict['givenName'][0].decode('ascii'), | |||||
'lastName': user_dict['sn'][0].decode('ascii'), | |||||
'email': user_dict['mail'][0].decode('ascii'), | |||||
'userName': user_dict['uid'][0].decode('ascii'), | |||||
} | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
c.execute("SELECT article_id, url, title, byline FROM saved_articles INNER JOIN articles on saved_articles.article_id = articles.id WHERE user=? AND read=0 OR read IS NULL", (session['user_id'], )) | |||||
rows = c.fetchall() | |||||
conn.commit() | |||||
conn.close() | |||||
return render_template('list.j2', articles = clean_articles(rows)) | |||||
@app.route('/archived') | |||||
@ldap.login_required | |||||
def archived(): | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
c.execute("SELECT article_id, url, title, byline FROM saved_articles INNER JOIN articles on saved_articles.article_id = articles.id WHERE user=? AND read=1", (session['user_id'], )) | |||||
rows = c.fetchall() | |||||
print(rows) | |||||
conn.commit() | |||||
conn.close() | |||||
return render_template('list.j2', articles = clean_articles(rows)) | |||||
@app.route('/save') | |||||
@ldap.login_required | |||||
def save(): | |||||
return render_template('save.j2') | |||||
@app.route('/login', methods=['GET', 'POST']) | |||||
def login(): | |||||
if g.user: | |||||
return redirect(url_for('index')) | |||||
if request.method == 'POST': | |||||
user = request.form['user'] | |||||
passwd = request.form['passwd'] | |||||
test = ldap.bind_user(user, passwd) | |||||
if test is None or passwd == '': | |||||
return render_template('login.j2', error='Invalid credentials') | |||||
else: | |||||
session['user_id'] = request.form['user'] | |||||
session['passwd'] = request.form['passwd'] | |||||
if session['next']: | |||||
next = session['next'] | |||||
session['next'] = '' | |||||
return redirect(next) | |||||
return redirect('/') | |||||
return render_template('login.j2') | |||||
@ldap.login_required | |||||
@app.route('/article/<int:article_id>') | |||||
def read_article(article_id): | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
c.execute("SELECT * FROM articles where id=?", (article_id,)) | |||||
rows = c.fetchall() | |||||
conn.commit() | |||||
conn.close() | |||||
if (len(rows) > 0): | |||||
return render_template('article.j2', article=rows[0]) | |||||
return render_template('article.j2', article=()) | |||||
@ldap.login_required | |||||
@app.route('/add', methods=['GET', 'POST']) | |||||
def add_url(): | |||||
if not 'user_id' in session: | |||||
session['next'] = request.url | |||||
return redirect(url_for('login')) | |||||
if request.method == 'POST': | |||||
url = request.form['url'] | |||||
close = None | |||||
else: | |||||
url = request.args.get('url') | |||||
close = request.args.get('close') | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
if url is not None and len(url) > 0: | |||||
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'} | |||||
response = requests.get(url, headers=headers) | |||||
article = simple_json_from_html_string(response.text, use_readability=True) | |||||
c.execute("SELECT * FROM articles WHERE url=?", (url,)) | |||||
rows = c.fetchall() | |||||
if (len(rows) == 0): | |||||
c.execute("INSERT INTO articles (url, content, title, byline) VALUES (?, ?, ?, ?)", (url, article['content'], article['title'], article['byline'])) | |||||
c.execute("SELECT * FROM articles WHERE url=?", (url,)) | |||||
rows = c.fetchall() | |||||
article_id = rows[0][0] | |||||
c.execute("SELECT * FROM saved_articles WHERE user=? AND article_id=?", (session['user_id'], article_id)) | |||||
rows = c.fetchall() | |||||
if (len(rows) == 0): | |||||
c.execute("INSERT INTO saved_articles (user, article_id) VALUES (?, ?)", (session['user_id'], article_id)) | |||||
conn.commit() | |||||
conn.close() | |||||
if close is not None and close == '1': | |||||
return render_template('close.j2') | |||||
return 'Saved' | |||||
conn.commit() | |||||
conn.close() | |||||
return 'Error' | |||||
@ldap.login_required | |||||
@app.route('/delete/<int:article_id>') | |||||
def delete_article(article_id): | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
c.execute("DELETE FROM saved_articles WHERE user=? AND article_id=?", (session['user_id'], article_id)) | |||||
c.execute("SELECT * FROM saved_articles WHERE article_id=?", (article_id, )) | |||||
rows = c.fetchall() | |||||
if (len(rows) == 0): | |||||
c.execute("DELETE FROM articles WHERE id=?", (article_id,)) | |||||
conn.commit() | |||||
conn.close() | |||||
return redirect(url_for('index')) | |||||
@ldap.login_required | |||||
@app.route('/archive/<int:article_id>') | |||||
def archive_article(article_id): | |||||
conn = sqlite3.connect('pocket/readitlater.db') | |||||
c = conn.cursor() | |||||
c.execute("UPDATE saved_articles SET read=1 WHERE user=? AND article_id=?", (session['user_id'], article_id)) | |||||
conn.commit() | |||||
conn.close() | |||||
return redirect(url_for('index')) | |||||
@app.route('/logout') | |||||
def logout(): | |||||
session.pop('user_id', None) | |||||
return redirect(url_for('index')) | |||||
if __name__ == '__main__': | |||||
app.run() |
@ -0,0 +1,4 @@ | |||||
Instapaper outdated | |||||
Pocket overrated | |||||
Long have we waited | |||||
Read TI Later _activated_ |
@ -0,0 +1 @@ | |||||
javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='http://127.0.0.1:5000/add?close=1&url='+encodeURIComponent(url);})(); |
@ -0,0 +1,8 @@ | |||||
Flask==1.1.2 | |||||
Flask-Bootstrap4==4.0.2 | |||||
Flask-Login==0.5.0 | |||||
Flask-SimpleLDAP==1.4.0 | |||||
python-ldap==3.2.0 | |||||
ldap3==2.7 | |||||
readability-lxml==0.8.1 | |||||
requests==2.24.0 |
@ -0,0 +1,197 @@ | |||||
.wrapper { | |||||
display: flex; | |||||
width: 100%; | |||||
align-items: stretch; | |||||
} | |||||
.toolbar-button { | |||||
padding-left: 2rem; | |||||
padding-top: .375rem; | |||||
line-height: 1.5; | |||||
color: white; | |||||
} | |||||
.back-chevron { | |||||
line-height: 1.5 !important; | |||||
vertical-align: middle !important; | |||||
margin-bottom: 4px !important; | |||||
} | |||||
#sidebar { | |||||
padding-left: 20px; | |||||
background-color: #ecedee; | |||||
height: 100vh; | |||||
padding-top: 20px; | |||||
border-right: 1px solid darkgray; | |||||
} | |||||
#sidebar ul > li { | |||||
padding-bottom: 10px; | |||||
} | |||||
#sidebar ul > li > a { | |||||
color: rgba(0,0,0,.5); | |||||
} | |||||
#sidebar ul > li.active > a { | |||||
color: black; | |||||
} | |||||
#content { | |||||
width: 100%; | |||||
padding: 20px; | |||||
} | |||||
@media (prefers-color-scheme: dark) { | |||||
#sidebar { | |||||
padding-left: 20px; | |||||
background-color: #1a1d20; | |||||
height: 100vh; | |||||
padding-top: 20px; | |||||
border-right: 1px solid black; | |||||
} | |||||
#sidebar ul > li { | |||||
padding-bottom: 10px; | |||||
} | |||||
#sidebar ul > li > a { | |||||
color: rgba(255,255,255,.5); | |||||
} | |||||
#sidebar ul > li.active > a { | |||||
color: white; | |||||
} | |||||
body { | |||||
background-color: #111 !important; | |||||
color: #eee; | |||||
} | |||||
.jumbotron { | |||||
background-color: #333 !important; | |||||
} | |||||
.modal-content { | |||||
background-color: #111 !important; | |||||
color: #eee; | |||||
} | |||||
.modal-header { | |||||
border-bottom: 1px solid #555 !important; | |||||
} | |||||
.modal-header .close { | |||||
color: #eee !important; | |||||
text-shadow: 0 1px 0 #555 !important; | |||||
} | |||||
.modal-footer { | |||||
border-top: 1px solid #555 !important; | |||||
} | |||||
.bg-light { | |||||
background-color: #333 !important; | |||||
} | |||||
.bg-white { | |||||
background-color: #000 !important; | |||||
} | |||||
.bg-black { | |||||
background-color: #eee !important; | |||||
} | |||||
.form-control { | |||||
display: block; | |||||
width: 100%; | |||||
height: calc(1.5em + 0.75rem + 2px); | |||||
padding: 0.375rem 0.75rem; | |||||
font-size: 1rem; | |||||
font-weight: 400; | |||||
line-height: 1.5; | |||||
color: #dee2e6; | |||||
background-color: #000; | |||||
background-clip: padding-box; | |||||
border: 1px solid #6c757d; | |||||
border-radius: 0.25rem; | |||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | |||||
} | |||||
@media (prefers-reduced-motion: reduce) { | |||||
.form-control { | |||||
transition: none; | |||||
} | |||||
} | |||||
.form-control::-ms-expand { | |||||
background-color: transparent; | |||||
border: 0; | |||||
} | |||||
.form-control:-moz-focusring { | |||||
color: transparent; | |||||
text-shadow: 0 0 0 #dee2e6; | |||||
} | |||||
.form-control:focus { | |||||
color: #dee2e6; | |||||
background-color: #191d21; | |||||
border-color: #b3d7ff; | |||||
outline: 0; | |||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | |||||
} | |||||
.form-control::-webkit-input-placeholder { | |||||
color: #6c757d; | |||||
opacity: 1; | |||||
} | |||||
.form-control::-moz-placeholder { | |||||
color: #6c757d; | |||||
opacity: 1; | |||||
} | |||||
.form-control::-ms-input-placeholder { | |||||
color: #6c757d; | |||||
opacity: 1; | |||||
} | |||||
.form-control::placeholder { | |||||
color: #6c757d; | |||||
opacity: 1; | |||||
} | |||||
.form-control:disabled, | |||||
.form-control[readonly] { | |||||
background-color: #343a40; | |||||
opacity: 1; | |||||
} | |||||
.card { | |||||
background-color: #000; | |||||
border: 1px solid #6c757d; | |||||
} | |||||
} | |||||
@media (max-width: 991.98px) { | |||||
#sidebar { | |||||
display: none !important; | |||||
} | |||||
} | |||||
@media (min-width: 991.98px) { | |||||
.navbar-nav { | |||||
display: none !important; | |||||
} | |||||
#sidebar { | |||||
min-width: 250px; | |||||
max-width: 250px; | |||||
display: block !important; | |||||
} | |||||
#sidebar.active { | |||||
margin-left: -250px; | |||||
} | |||||
} |
@ -0,0 +1,60 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Read TI Later{% endblock %} | |||||
{% block styles %} | |||||
{{super()}} | |||||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||||
{% endblock %} | |||||
{% block navbar %} | |||||
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||||
<a href="/"><div class="navbar-brand"><i data-feather="arrow-left" style="line-height: 1.5; vertical-align: middle; margin-bottom: 4px;"></i></div></a> | |||||
<form class="form-inline ml-auto"> | |||||
{# <div class="custom-control custom-switch"> | |||||
<input type="checkbox" class="custom-control-input" id="darkSwitch" /> | |||||
<label class="custom-control-label" for="darkSwitch">Dark Mode</label> | |||||
</div> | |||||
<div class="navbar-text" style="margin-right: 20px; ">{{ user['userName'] }}</div> #} | |||||
<a class="toolbar-button" href="#"><i data-feather="type"></i></a> | |||||
<a class="toolbar-button" href="/archive/{{ article[0] }}"><i data-feather="archive"></i></a> | |||||
<a class="toolbar-button" href="/delete/{{ article[0] }}" style="padding-right: 1rem;"><i data-feather="trash"></i></a> | |||||
</form> | |||||
</nav> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<div class="container" style="margin-top: 15px"> | |||||
<div id="error-alert" class="alert alert-danger" role="alert" style="display: none;"> | |||||
This is a danger alert—check it out! | |||||
</div> | |||||
<form> | |||||
<div class="row justify-content-center"> | |||||
{# <div class="col-lg-6"> | |||||
<label for="formGroupExampleInput4">URL</label> | |||||
<input id="link-form" type="text" class="form-control" placeholder="https://example.com"> | |||||
<br> | |||||
<button type="button" class="btn btn-primary" onclick="addUrl();">Add</button> | |||||
<br> | |||||
<br> | |||||
</div> #} | |||||
<div class="col-lg-12"> | |||||
<div style="padding: 10px;" id="article"> | |||||
<h1 style="text-align: center;">{{ article[3] }}</h1> | |||||
<a href="{{ article[1] }}" class="text-info" target="_blank" style="display: block; text-align: center; margin-bottom: 20px;">View Original</a> | |||||
{{ article[2] | safe}} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
{% endblock %} | |||||
{% block scripts %} | |||||
{{ super() }} | |||||
<script src="https://unpkg.com/feather-icons"></script> | |||||
<script> | |||||
$('#article img').css('max-width', '100%'); | |||||
$('#article img').css('height', 'auto'); | |||||
feather.replace(); | |||||
</script> | |||||
{% endblock %} |
@ -0,0 +1,3 @@ | |||||
<script> | |||||
window.close(); | |||||
</script> |
@ -0,0 +1,93 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Read TI Later{% endblock %} | |||||
{% block styles %} | |||||
{{super()}} | |||||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||||
{% endblock %} | |||||
{% block navbar %} | |||||
<div id="navbar" class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||||
<div class="navbar-brand">Read TI Later</div> | |||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" | |||||
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> | |||||
<span class="navbar-toggler-icon"></span> | |||||
</button> | |||||
<div class="collapse navbar-collapse" id="navbarNavDropdown"> | |||||
<ul class="navbar-nav"> | |||||
{% if request.path == url_for('index') %} | |||||
<li class="nav-item active"> | |||||
{% else %} | |||||
<li class="nav-item"> | |||||
{% endif %} | |||||
<a class="nav-link" href="{{ url_for('index') }}">My List</a> | |||||
</li> | |||||
{% if request.path == url_for('archived') %} | |||||
<li class="nav-item active"> | |||||
{% else %} | |||||
<li class="nav-item"> | |||||
{% endif %} | |||||
<a class="nav-link" href="{{ url_for('archived') }}">Archived</a> | |||||
</li> | |||||
</ul> | |||||
<form class="form-inline ml-auto"> | |||||
<a class="btn btn-primary" href="/save" role="button" style="margin-right: 20px;">Save</a> | |||||
<a class="btn btn-primary" href="/logout" role="button">Logout</a> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<div class="wrapper"> | |||||
<!-- Sidebar --> | |||||
<div id="sidebar"> | |||||
<ul class="list-unstyled components"> | |||||
{% if request.path == url_for('index') %} | |||||
<li class="nav-item active"> | |||||
{% else %} | |||||
<li class="nav-item"> | |||||
{% endif %} | |||||
<a href="{{ url_for('index') }}"><i data-feather="home" style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"></i>My List</a> | |||||
</li> | |||||
{% if request.path == url_for('archived') %} | |||||
<li class="nav-item active"> | |||||
{% else %} | |||||
<li class="nav-item"> | |||||
{% endif %} | |||||
<a href="{{ url_for('archived') }}"><i data-feather="archive" style="height: 1rem; vertical-align: middle; margin-bottom: 4px;"></i>Archived</a> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
<!-- Page Content --> | |||||
<div id="content"> | |||||
<form> | |||||
<div class="row justify-content-center"> | |||||
<div class="col-lg-12"> | |||||
{% for article in articles|reverse %} | |||||
<div style="color: black; padding: 10px; margin-bottom: 10px" class="card" id="article"> | |||||
<a href="/article/{{article[0]}}">{{ article[2] }}</a> | |||||
<a href="{{ article[1] }}" target="_blank" style="color: darkgray">{{ article[4] }}</a> | |||||
<br> | |||||
<span> | |||||
<a href="/archive/{{article[0]}}" class='text-success'>Archive</a> | |||||
| |||||
<a href="/delete/{{article[0]}}" class='text-danger'>Delete</a> | |||||
</span> | |||||
</div> | |||||
{% endfor %} | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block scripts %} | |||||
{{ super() }} | |||||
<script src="https://unpkg.com/feather-icons"></script> | |||||
<script> | |||||
feather.replace(); | |||||
</script> | |||||
{% endblock %} |
@ -0,0 +1,37 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Technical Incompetence Link Shortener{% endblock %} | |||||
{% block styles %} | |||||
{{super()}} | |||||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||||
{% endblock %} | |||||
{% block navbar %} | |||||
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||||
<div class="navbar-brand">Technical Incompetence Link Shortener</div> | |||||
</nav> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<div class="container" style="margin-top: 15px"> | |||||
<div class="jumbotron"> | |||||
<h1>Sign in for our awesome service</h1> | |||||
<p>Forgot your password? Too bad! We don't have emails working yet!</p> | |||||
</div> | |||||
<div class="col-md-12"> | |||||
{% if error is defined %} | |||||
<div id="error-alert" class="alert alert-danger" role="alert"> | |||||
{{ error }} | |||||
</div> | |||||
{% endif %} | |||||
<form action="" method="post"> | |||||
<label for="exampleFormControlInput1">Username</label> | |||||
<input name="user" class="form-control"><br> | |||||
<label for="exampleFormControlInput2">Password</label> | |||||
<input type="password" name="passwd" class="form-control"><br> | |||||
<button type="submit" class="btn btn-primary">Log In</button> | |||||
</form> | |||||
</div> | |||||
{% endblock %} |
@ -0,0 +1,118 @@ | |||||
{% extends "bootstrap/base.html" %} | |||||
{% block title %}Read TI Later{% endblock %} | |||||
{% block styles %} | |||||
{{super()}} | |||||
<link rel="stylesheet" href="{{url_for('.static', filename='style.css')}}"> | |||||
{% endblock %} | |||||
{% block navbar %} | |||||
<div id="navbar" class="navbar navbar-expand-lg sticky-top navbar-dark bg-dark"> | |||||
<div class="navbar-brand">Read TI Later</div> | |||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" | |||||
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> | |||||
<span class="navbar-toggler-icon"></span> | |||||
</button> | |||||
<div class="collapse navbar-collapse" id="navbarNavDropdown"> | |||||
<ul class="navbar-nav"> | |||||
<li class="nav-item"> | |||||
<a class="nav-link" href="{{ url_for('index') }}">My List</a> | |||||
</li> | |||||
<li class="nav-item"> | |||||
<a class="nav-link" href="{{ url_for('archived') }}">Archived</a> | |||||
</li> | |||||
</ul> | |||||
<form class="form-inline ml-auto"> | |||||
<a class="btn btn-primary" href="/logout" role="button">Logout</a> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block content %} | |||||
<div class="wrapper"> | |||||
<!-- Sidebar --> | |||||
<div id="sidebar"> | |||||
<ul class="list-unstyled components"> | |||||
<li> | |||||
<a href="/">My List</a> | |||||
</li> | |||||
<li> | |||||
<a href="/archived">Archived</a> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
<!-- Page Content --> | |||||
<div id="content"> | |||||
<div id="error-alert" class="alert alert-danger" role="alert" style="display: none;"> | |||||
This is a danger alert—check it out! | |||||
</div> | |||||
<form> | |||||
<div class="row justify-content-center"> | |||||
<div class="col-lg-8"> | |||||
<label for="formGroupExampleInput4">URL</label> | |||||
<input id="link-form" type="text" class="form-control" placeholder="https://example.com"> | |||||
<br> | |||||
<button type="button" class="btn btn-primary" onclick="addUrl();">Add</button> | |||||
<br> | |||||
<br> | |||||
</div> | |||||
<div style="position:fixed; bottom: 20px;"> | |||||
<p style="text-align: center;">Would you rather save on the go? Try our bookmarklet!</p> | |||||
<code> | |||||
javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='{{ request.url_root }}add?close=1&url='+encodeURIComponent(url);})(); | |||||
</code> | |||||
</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} | |||||
{% block scripts %} | |||||
{{ super() }} | |||||
<script> | |||||
function addUrl() { | |||||
url = $('#link-form').val(); | |||||
if (url.trim().length === 0) { | |||||
showError('URL is required'); | |||||
return false; | |||||
} | |||||
$.ajax({ | |||||
url: '/add', | |||||
method: 'POST', | |||||
data: { "url": url }, | |||||
success: function(data) { | |||||
if (data !== 'Error') | |||||
window.location.reload(); | |||||
else | |||||
showError('URL cannot be empty'); | |||||
} | |||||
}); | |||||
} | |||||
function showError(error) { | |||||
hideSuccess(); | |||||
$('#error-alert').text(error); | |||||
$('#error-alert').show(); | |||||
} | |||||
function showArticle(message) { | |||||
hideError(); | |||||
$('#article').html(message); | |||||
$('#article img').css('max-width', '100%') | |||||
$('#article').show(); | |||||
} | |||||
function hideError(error) { | |||||
$('#error-alert').hide(); | |||||
} | |||||
function hideArticle(error) { | |||||
$('#article').hide(); | |||||
} | |||||
</script> | |||||
{% endblock %} |
@ -0,0 +1,12 @@ | |||||
from urllib.parse import urlparse | |||||
def clean_articles(rows): | |||||
#article_id, url, title, byline | |||||
out = [] | |||||
for row in rows: | |||||
parsed_uri = urlparse(row[1]) | |||||
result = '{uri.netloc}'.format(uri=parsed_uri) | |||||
out.append([row[0], row[1], row[2], row[3], result]) | |||||
return out |