Implemented calendar
This commit is contained in:
parent
d2cef14c38
commit
6f53cd3836
15 changed files with 145 additions and 182 deletions
|
|
@ -8,7 +8,12 @@
|
|||
+ Added `User` table.
|
||||
+ Added `Flask-Login` and `Flask-WTF` dependencies in order to implement user logins.
|
||||
+ Added `python-i18n` as a dependency. Therefore, i18n changed format, using JSON files now.
|
||||
+ Now you can export pages in a JSON format. Coming soon: importing.
|
||||
+ Login is now required for creating and editing.
|
||||
+ Now you can leave a comment while changing a page’s text. Moreover, a new revision is created now
|
||||
only in case of an effective text change.
|
||||
+ Now a page can be dated in the calendar.
|
||||
+ Now you can export and import pages in a JSON format. Importing can be done by admin users only.
|
||||
+ Improved page history view, and added user contributions page.
|
||||
+ Like it or not, now gzip library is required.
|
||||
+ Added CSS variables in the site style.
|
||||
|
||||
|
|
|
|||
33
app.py
33
app.py
|
|
@ -21,7 +21,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||
from werkzeug.routing import BaseConverter
|
||||
from peewee import *
|
||||
from playhouse.db_url import connect as dbconnect
|
||||
import datetime, hashlib, html, importlib, json, markdown, os, random, \
|
||||
import calendar, datetime, hashlib, html, importlib, json, markdown, os, random, \
|
||||
re, sys, warnings
|
||||
from functools import lru_cache, partial
|
||||
from urllib.parse import quote
|
||||
|
|
@ -436,10 +436,10 @@ def is_url_available(url):
|
|||
return url not in forbidden_urls and not Page.select().where(Page.url == url).exists()
|
||||
|
||||
forbidden_urls = [
|
||||
'create', 'edit', 'p', 'ajax', 'history', 'manage', 'static', 'media',
|
||||
'accounts', 'tags', 'init-config', 'upload', 'upload-info', 'about',
|
||||
'stats', 'terms', 'privacy', 'easter', 'search', 'help', 'circles',
|
||||
'protect', 'kt', 'embed', 'backlinks', 'u'
|
||||
'about', 'accounts', 'ajax', 'backlinks', 'calendar', 'circles', 'create',
|
||||
'easter', 'edit', 'embed', 'help', 'history', 'init-config', 'kt',
|
||||
'manage', 'media', 'p', 'privacy', 'protect', 'search', 'static', 'stats',
|
||||
'tags', 'terms', 'u', 'upload', 'upload-info'
|
||||
]
|
||||
|
||||
app = Flask(__name__)
|
||||
|
|
@ -547,6 +547,7 @@ def savepoint(form, is_preview=False, pageobj=None):
|
|||
pl_owner_is_current_user=pageobj.is_owned_by(current_user) if pageobj else True,
|
||||
preview=preview,
|
||||
pl_js_info=pl_js_info,
|
||||
pl_calendar=('usecalendar' in form) and form['calendar'],
|
||||
pl_readonly=not pageobj.can_edit(current_user) if pageobj else False
|
||||
)
|
||||
|
||||
|
|
@ -575,7 +576,8 @@ def create():
|
|||
title=request.form['title'],
|
||||
is_redirect=False,
|
||||
touched=datetime.datetime.now(),
|
||||
owner=current_user,
|
||||
owner_id=current_user.id,
|
||||
calendar=datetime.date.fromisoformat(request.form["calendar"]) if 'usecalendar' in request.form else None,
|
||||
is_math_enabled='enablemath' in request.form,
|
||||
is_locked = 'lockpage' in request.form
|
||||
)
|
||||
|
|
@ -632,6 +634,7 @@ def edit(id):
|
|||
p.touched = datetime.datetime.now()
|
||||
p.is_math_enabled = 'enablemath' in request.form
|
||||
p.is_locked = 'lockpage' in request.form
|
||||
p.calendar = datetime.date.fromisoformat(request.form["calendar"]) if 'usecalendar' in request.form else None
|
||||
p.save()
|
||||
p.change_tags(p_tags)
|
||||
if request.form['text'] != p.latest.text:
|
||||
|
|
@ -657,6 +660,12 @@ def edit(id):
|
|||
form["enablemath"] = "1"
|
||||
if p.is_locked:
|
||||
form["lockpage"] = "1"
|
||||
if p.calendar:
|
||||
form["usecalendar"] = "1"
|
||||
try:
|
||||
form["calendar"] = p.calendar.isoformat().split("T")[0]
|
||||
except Exception:
|
||||
form["calendar"] = p.calendar
|
||||
|
||||
return savepoint(form, pageobj=p)
|
||||
|
||||
|
|
@ -778,6 +787,18 @@ def contributions(username):
|
|||
abort(404)
|
||||
return render_template('contributions.html', u=user, contributions=user.contributions.order_by(PageRevision.pub_date.desc()))
|
||||
|
||||
@app.route('/calendar/')
|
||||
def calendar_view():
|
||||
return render_template('calendar.html')
|
||||
|
||||
@app.route('/calendar/<int:y>/<int:m>')
|
||||
def calendar_month(y, m):
|
||||
notes = Page.select().where(
|
||||
(datetime.date(y, m, 1) <= Page.calendar) &
|
||||
(Page.calendar < datetime.date(y+1 if m==12 else y, 1 if m==12 else m+1, 1))
|
||||
).order_by(Page.calendar)
|
||||
|
||||
return render_template('month.html', d=datetime.date(y, m, 1), notes=notes)
|
||||
|
||||
@app.route('/history/revision/<int:revisionid>/')
|
||||
def view_old(revisionid):
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
from flask import Blueprint, render_template
|
||||
from peewee import *
|
||||
import instagram_private_api, json, os, sys, random, codecs
|
||||
|
||||
database = SqliteDatabase('instagram.sqlite')
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
class InstagramProfile(BaseModel):
|
||||
p_id = IntegerField()
|
||||
p_username = CharField(30)
|
||||
p_full_name = CharField(30)
|
||||
p_biography = CharField(150)
|
||||
posts_count = IntegerField()
|
||||
followers_count = IntegerField()
|
||||
following_count = IntegerField()
|
||||
flags = BitField()
|
||||
pub_date = DateTimeField()
|
||||
is_verified = flags.flag(1)
|
||||
is_private = flags.flag(2)
|
||||
|
||||
class InstagramMedia(BaseModel):
|
||||
user = IntegerField()
|
||||
pub_date = DateTimeField()
|
||||
media_url = TextField()
|
||||
description = CharField(2200)
|
||||
|
||||
def init_db():
|
||||
database.create_tables([InstagramProfile, InstagramMedia])
|
||||
|
||||
def bytes_to_json(python_object):
|
||||
if isinstance(python_object, bytes):
|
||||
return {'__class__': 'bytes',
|
||||
'__value__': codecs.encode(python_object, 'base64').decode()}
|
||||
raise TypeError(repr(python_object) + ' is not JSON serializable')
|
||||
|
||||
def bytes_from_json(json_object):
|
||||
if '__class__' in json_object and json_object['__class__'] == 'bytes':
|
||||
return codecs.decode(json_object['__value__'].encode(), 'base64')
|
||||
return json_object
|
||||
|
||||
SETTINGS_PATH = 'ig_api_settings'
|
||||
|
||||
def load_settings(username):
|
||||
with open(os.path.join(SETTINGS_PATH, username + '.json')) as f:
|
||||
settings = json.load(f, object_hook=bytes_from_json)
|
||||
return settings
|
||||
|
||||
def save_settings(username, settings):
|
||||
with open(os.path.join(SETTINGS_PATH, username + '.json'), 'w') as f:
|
||||
json.dump(settings, f, default=bytes_to_json)
|
||||
|
||||
CLIENTS = []
|
||||
|
||||
def load_clients():
|
||||
try:
|
||||
with open(os.path.join(SETTINGS_PATH, 'config.txt')) as f:
|
||||
conf = f.read()
|
||||
except OSError:
|
||||
print('Config file not found.')
|
||||
return
|
||||
for up in conf.split('\n'):
|
||||
try:
|
||||
up = up.split('#')[0].strip()
|
||||
if not up:
|
||||
continue
|
||||
username, password = up.split(':')
|
||||
try:
|
||||
settings = load_settings(username)
|
||||
except Exception:
|
||||
settings = None
|
||||
try:
|
||||
if settings:
|
||||
device_id = settings.get('device_id')
|
||||
api = instagram_private_api.Client(
|
||||
username, password,
|
||||
settings=settings
|
||||
)
|
||||
else:
|
||||
api = instagram_private_api.Client(
|
||||
username, password,
|
||||
on_login=lambda x: save_settings(username, x.settings)
|
||||
)
|
||||
except (instagram_private_api.ClientCookieExpiredError,
|
||||
instagram_private_api.ClientLoginRequiredError) as e:
|
||||
api = instagram_private_api.Client(
|
||||
username, password,
|
||||
device_id=device_id,
|
||||
on_login=lambda x: save_settings(username, x.settings)
|
||||
)
|
||||
CLIENTS.append(api)
|
||||
except Exception:
|
||||
sys.excepthook(*sys.exc_info())
|
||||
continue
|
||||
|
||||
def make_request(method_name, *args, **kwargs):
|
||||
exc = None
|
||||
usable_clients = list(range(len(CLIENTS)))
|
||||
while usable_clients:
|
||||
ci = random.choice(usable_clients)
|
||||
client = CLIENTS[ci]
|
||||
usable_clients.remove(ci)
|
||||
try:
|
||||
method = getattr(client, method_name)
|
||||
except AttributeError:
|
||||
raise ValueError('client has no method called {!r}'.format(method_name))
|
||||
if not callable(method):
|
||||
raise ValueError('client has no method called {!r}'.format(method_name))
|
||||
try:
|
||||
return method(*args, **kwargs)
|
||||
except Exception as e:
|
||||
exc = e
|
||||
if exc:
|
||||
raise exc
|
||||
else:
|
||||
raise RuntimeError('no active clients')
|
||||
|
||||
N_FORCE = 0
|
||||
N_FALLBACK_CACHE = 1
|
||||
N_PREFER_CACHE = 2
|
||||
N_OFFLINE = 3
|
||||
|
||||
def choose_method(online, offline, network):
|
||||
if network == N_FORCE:
|
||||
return online()
|
||||
elif network == N_FALLBACK_CACHE:
|
||||
try:
|
||||
return online()
|
||||
except Exception:
|
||||
return offline()
|
||||
elif network == N_PREFER_CACHE:
|
||||
try:
|
||||
return offline()
|
||||
except Exception:
|
||||
return online()
|
||||
elif network == N_OFFLINE:
|
||||
return offline()
|
||||
|
||||
def get_profile_info(username_or_id, network=N_FALLBACK_CACHE):
|
||||
if isinstance(username_or_id, str):
|
||||
username, userid = username_or_id, None
|
||||
elif isinstance(username_or_id, int):
|
||||
username, userid = None, username_or_id
|
||||
else:
|
||||
raise TypeError('invalid username or id')
|
||||
def online():
|
||||
if userid:
|
||||
data = make_request('user_info', userid)
|
||||
else:
|
||||
data = make_request('username_info', username)
|
||||
return InstagramProfile.create(
|
||||
p_id = data['user']['pk'],
|
||||
p_username = data['user']['username'],
|
||||
p_full_name = data['user']['full_name'],
|
||||
p_biography = data['user']['biography'],
|
||||
posts_count = data['user']['media_count'],
|
||||
followers_count = data['user']['follower_count'],
|
||||
following_count = data['user']['following_count'],
|
||||
is_verified = data['user']['is_verified'],
|
||||
is_private = data['user']['is_private'],
|
||||
pub_date = datetime.datetime.now()
|
||||
)
|
||||
def offline():
|
||||
if userid:
|
||||
q = InstagramProfile.select().where(InstagramProfile.p_id == userid)
|
||||
else:
|
||||
q = InstagramProfile.select().where(InstagramProfile.p_username == username)
|
||||
return q.order_by(InstagramProfile.pub_date.desc())[0]
|
||||
return choose_method(online, offline, network)
|
||||
|
||||
load_clients()
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
"random-page": "Random page",
|
||||
"search": "Search",
|
||||
"year": "Year",
|
||||
"month": "Month",
|
||||
"calculate": "Calculate",
|
||||
"show-all": "Show all",
|
||||
"just-now": "just now",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"random-page": "Pagina casuale",
|
||||
"search": "Cerca",
|
||||
"year": "Anno",
|
||||
"month": "Mese",
|
||||
"calculate": "Calcola",
|
||||
"show-all": "Mostra tutto",
|
||||
"just-now": "poco fa",
|
||||
|
|
|
|||
|
|
@ -70,8 +70,13 @@ input[type="submit"],input[type="reset"],input[type="button"],button{font-family
|
|||
.page-tags .tag-count{color: var(--btn-success);font-size:smaller;font-weight:600}
|
||||
.search-wrapper {display:flex;width:90%;margin:auto}
|
||||
.search-wrapper > input {flex:1}
|
||||
.calendar-subtitle {text-align: center; margin-top: -1em}
|
||||
.preview-subtitle {text-align: center; margin-top: -1em}
|
||||
textarea {background-color: var(--bg-sharp); color: var(--fg-sharp)}
|
||||
ul.inline {margin:0; padding:0; display: inline}
|
||||
ul.inline > li {display: inline-block;}
|
||||
ul.inline > li::after {content: "·"}
|
||||
ul.inline > li:last-child::after {content: ""}
|
||||
|
||||
/* Circles extension */
|
||||
.circles-red{color: #e14}
|
||||
|
|
|
|||
33
templates/calendar.html
Normal file
33
templates/calendar.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Calendar – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Calendar</h1>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>Calendar view</legend>
|
||||
|
||||
<div>
|
||||
<label><strong>Show as</strong>:</label>
|
||||
<ul class="inline">
|
||||
{% set view_choices = ["month"] %}
|
||||
{% for vch in view_choices %}
|
||||
<li>
|
||||
{% if vch == viewas %}
|
||||
<strong>{{ T(vch) }}</strong>
|
||||
{% else %}
|
||||
<a href="/calendar/{{ vch }}">{{ T(vch) }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
|
||||
<div>
|
||||
<label><strong>Show month</strong></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -77,6 +77,11 @@
|
|||
<label for="CB__lockpage">Lock page for editing by other users</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<input type="checkbox" id="CB__usecalendar" name="usecalendar" {% if pl_calendar %}checked=""{% endif %}>
|
||||
<label for="CB__usecalendar">Use calendar:</label>
|
||||
<input type="date" name="calendar" {% if pl_calendar %}value="{{ pl_calendar }}"{% endif %}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@
|
|||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if n.calendar %}
|
||||
<p class="nl-calendar">
|
||||
<span class="material-icons">calendar_today</span>
|
||||
<time datetime="{{ n.calendar.isoformat() }}">{{ n.calendar.strftime('%B %-d, %Y') }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a href="/p/most_recent/">{{ T('show-all') }}</a></li>
|
||||
|
|
|
|||
|
|
@ -12,3 +12,9 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% if n.calendar %}
|
||||
<p class="nl-calendar">
|
||||
<span class="material-icons">calendar_today</span>
|
||||
<time datetime="{{ n.calendar.isoformat() }}">{{ n.calendar.strftime('%B %-d, %Y') }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@
|
|||
<li>
|
||||
<a href="{{ n.get_url() }}" class="nl-title">{{ n.title }}</a>
|
||||
<p class="nl-desc">{{ n.short_desc() }}</p>
|
||||
<p class="nl-tags">Tags:
|
||||
{% if n.tags %}
|
||||
<p class="nl-tags">{{ T('tags') }}:
|
||||
{% for tag in n.tags %}
|
||||
<a href="/tags/{{ tag.name }}/" class="nl-tag">#{{ tag.name }}</a>
|
||||
{% set tn = tag.name %}
|
||||
<a href="/tags/{{ tn }}/" class="nl-tag">#{{ tn }}</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if n.calendar %}
|
||||
<p class="nl-calendar">
|
||||
<span class="material-icons">calendar_today</span>
|
||||
<time datetime="{{ n.calendar.isoformat() }}">{{ n.calendar.strftime('%B %-d, %Y') }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if page_n <= total_count // 20 %}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@
|
|||
<a href="/tags/{{ tn }}/" class="nl-tag">#{{ tn }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if n.calendar %}
|
||||
<p class="nl-calendar">
|
||||
<span class="material-icons">calendar_today</span>
|
||||
<time datetime="{{ n.calendar.isoformat() }}">{{ n.calendar.strftime('%B %-d, %Y') }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
|||
32
templates/month.html
Normal file
32
templates/month.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ d.strftime("%B %Y") }} – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ d.strftime("%B %Y") }}</h1>
|
||||
|
||||
<ul>
|
||||
{% for n in notes %}
|
||||
<li>
|
||||
<a href="{{ n.get_url() }}" class="nl-title">{{ n.title }}</a>
|
||||
<p class="nl-desc">{{ n.short_desc() }}</p>
|
||||
{% if n.tags %}
|
||||
<p class="nl-tags">{{ T('tags') }}:
|
||||
{% for tag in n.tags %}
|
||||
{% set tn = tag.name %}
|
||||
<a href="/tags/{{ tn }}/" class="nl-tag">#{{ tn }}</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if n.calendar %}
|
||||
<p class="nl-calendar">
|
||||
<span class="material-icons">calendar_today</span>
|
||||
<time datetime="{{ n.calendar.isoformat() }}">{{ n.calendar.strftime('%B %-d, %Y') }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -27,6 +27,8 @@
|
|||
</ul>
|
||||
{% elif q %}
|
||||
<h2>{{ T('search-no-results') }} <em>{{ q }}</em></h2>
|
||||
{% else %}
|
||||
<p>Please note that search queries do not search for page text.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
<article>
|
||||
<h1 id="firstHeading">{{ p.title }}</h1>
|
||||
|
||||
{% if p.calendar %}
|
||||
<p class="calendar-subtitle"><span class="material-icons">calendar_today</span>{{ p.calendar.strftime('%B %-d, %Y') }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="jump-to-actions">
|
||||
<span>{{ T('last-changed') }} {{ rev.human_pub_date() }}</span> ·
|
||||
<a href="#page-actions">{{ T('jump-to-actions') }}</a>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue