Implemented calendar

This commit is contained in:
Yusur 2023-02-10 14:15:21 +01:00
parent d2cef14c38
commit 6f53cd3836
15 changed files with 145 additions and 182 deletions

View file

@ -8,7 +8,12 @@
+ Added `User` table. + Added `User` table.
+ Added `Flask-Login` and `Flask-WTF` dependencies in order to implement user logins. + 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. + 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 pages 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. + Like it or not, now gzip library is required.
+ Added CSS variables in the site style. + Added CSS variables in the site style.

33
app.py
View file

@ -21,7 +21,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from peewee import * from peewee import *
from playhouse.db_url import connect as dbconnect 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 re, sys, warnings
from functools import lru_cache, partial from functools import lru_cache, partial
from urllib.parse import quote 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() return url not in forbidden_urls and not Page.select().where(Page.url == url).exists()
forbidden_urls = [ forbidden_urls = [
'create', 'edit', 'p', 'ajax', 'history', 'manage', 'static', 'media', 'about', 'accounts', 'ajax', 'backlinks', 'calendar', 'circles', 'create',
'accounts', 'tags', 'init-config', 'upload', 'upload-info', 'about', 'easter', 'edit', 'embed', 'help', 'history', 'init-config', 'kt',
'stats', 'terms', 'privacy', 'easter', 'search', 'help', 'circles', 'manage', 'media', 'p', 'privacy', 'protect', 'search', 'static', 'stats',
'protect', 'kt', 'embed', 'backlinks', 'u' 'tags', 'terms', 'u', 'upload', 'upload-info'
] ]
app = Flask(__name__) 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, pl_owner_is_current_user=pageobj.is_owned_by(current_user) if pageobj else True,
preview=preview, preview=preview,
pl_js_info=pl_js_info, 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 pl_readonly=not pageobj.can_edit(current_user) if pageobj else False
) )
@ -575,7 +576,8 @@ def create():
title=request.form['title'], title=request.form['title'],
is_redirect=False, is_redirect=False,
touched=datetime.datetime.now(), 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_math_enabled='enablemath' in request.form,
is_locked = 'lockpage' in request.form is_locked = 'lockpage' in request.form
) )
@ -632,6 +634,7 @@ def edit(id):
p.touched = datetime.datetime.now() p.touched = datetime.datetime.now()
p.is_math_enabled = 'enablemath' in request.form p.is_math_enabled = 'enablemath' in request.form
p.is_locked = 'lockpage' 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.save()
p.change_tags(p_tags) p.change_tags(p_tags)
if request.form['text'] != p.latest.text: if request.form['text'] != p.latest.text:
@ -657,6 +660,12 @@ def edit(id):
form["enablemath"] = "1" form["enablemath"] = "1"
if p.is_locked: if p.is_locked:
form["lockpage"] = "1" 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) return savepoint(form, pageobj=p)
@ -778,6 +787,18 @@ def contributions(username):
abort(404) abort(404)
return render_template('contributions.html', u=user, contributions=user.contributions.order_by(PageRevision.pub_date.desc())) 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>/') @app.route('/history/revision/<int:revisionid>/')
def view_old(revisionid): def view_old(revisionid):

View file

@ -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()

View file

@ -23,6 +23,7 @@
"random-page": "Random page", "random-page": "Random page",
"search": "Search", "search": "Search",
"year": "Year", "year": "Year",
"month": "Month",
"calculate": "Calculate", "calculate": "Calculate",
"show-all": "Show all", "show-all": "Show all",
"just-now": "just now", "just-now": "just now",

View file

@ -23,6 +23,7 @@
"random-page": "Pagina casuale", "random-page": "Pagina casuale",
"search": "Cerca", "search": "Cerca",
"year": "Anno", "year": "Anno",
"month": "Mese",
"calculate": "Calcola", "calculate": "Calcola",
"show-all": "Mostra tutto", "show-all": "Mostra tutto",
"just-now": "poco fa", "just-now": "poco fa",

View file

@ -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} .page-tags .tag-count{color: var(--btn-success);font-size:smaller;font-weight:600}
.search-wrapper {display:flex;width:90%;margin:auto} .search-wrapper {display:flex;width:90%;margin:auto}
.search-wrapper > input {flex:1} .search-wrapper > input {flex:1}
.calendar-subtitle {text-align: center; margin-top: -1em}
.preview-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)} 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 extension */
.circles-red{color: #e14} .circles-red{color: #e14}

33
templates/calendar.html Normal file
View 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 %}

View file

@ -77,6 +77,11 @@
<label for="CB__lockpage">Lock page for editing by other users</label> <label for="CB__lockpage">Lock page for editing by other users</label>
</div> </div>
{% endif %} {% 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 %} {% endif %}
</form> </form>

View file

@ -25,6 +25,12 @@
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% 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> </li>
{% endfor %} {% endfor %}
<li><a href="/p/most_recent/">{{ T('show-all') }}</a></li> <li><a href="/p/most_recent/">{{ T('show-all') }}</a></li>

View file

@ -12,3 +12,9 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </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 %}

View file

@ -13,11 +13,20 @@
<li> <li>
<a href="{{ n.get_url() }}" class="nl-title">{{ n.title }}</a> <a href="{{ n.get_url() }}" class="nl-title">{{ n.title }}</a>
<p class="nl-desc">{{ n.short_desc() }}</p> <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 %} {% 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 %} {% endfor %}
</p> </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> </li>
{% endfor %} {% endfor %}
{% if page_n <= total_count // 20 %} {% if page_n <= total_count // 20 %}

View file

@ -25,6 +25,12 @@
<a href="/tags/{{ tn }}/" class="nl-tag">#{{ tn }}</a> <a href="/tags/{{ tn }}/" class="nl-tag">#{{ tn }}</a>
{% endif %} {% endif %}
{% endfor %} {% 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> </p>
</li> </li>
{% endfor %} {% endfor %}

32
templates/month.html Normal file
View 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 %}

View file

@ -27,6 +27,8 @@
</ul> </ul>
{% elif q %} {% elif q %}
<h2>{{ T('search-no-results') }} <em>{{ q }}</em></h2> <h2>{{ T('search-no-results') }} <em>{{ q }}</em></h2>
{% else %}
<p>Please note that search queries do not search for page text.</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -7,6 +7,10 @@
{% block content %} {% block content %}
<article> <article>
<h1 id="firstHeading">{{ p.title }}</h1> <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"> <div class="jump-to-actions">
<span>{{ T('last-changed') }} {{ rev.human_pub_date() }}</span> · <span>{{ T('last-changed') }} {{ rev.human_pub_date() }}</span> ·