Adding some extensions and appearance improvements.
This commit is contained in:
parent
ec720743b3
commit
479d8eecc0
13 changed files with 506 additions and 12 deletions
32
app.py
32
app.py
|
|
@ -12,7 +12,7 @@ Extensions are supported (?), kept in extensions/ folder.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, abort, flash, g, jsonify, make_response, redirect, request,
|
Flask, Markup, abort, flash, g, jsonify, make_response, redirect, request,
|
||||||
render_template, send_from_directory)
|
render_template, send_from_directory)
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
from peewee import *
|
from peewee import *
|
||||||
|
|
@ -33,7 +33,7 @@ try:
|
||||||
except Exception:
|
except Exception:
|
||||||
markdown_strikethrough = None
|
markdown_strikethrough = None
|
||||||
|
|
||||||
__version__ = '0.3.0'
|
__version__ = '0.4-dev'
|
||||||
|
|
||||||
#### CONSTANTS ####
|
#### CONSTANTS ####
|
||||||
|
|
||||||
|
|
@ -205,6 +205,20 @@ class PageRevision(BaseModel):
|
||||||
return self.textref.get_content()
|
return self.textref.get_content()
|
||||||
def html(self):
|
def html(self):
|
||||||
return md(self.text)
|
return md(self.text)
|
||||||
|
def human_pub_date(self):
|
||||||
|
delta = datetime.datetime.now() - self.pub_date
|
||||||
|
T = partial(get_string, g.lang)
|
||||||
|
if delta < datetime.timedelta(seconds=60):
|
||||||
|
return T('just-now')
|
||||||
|
elif delta < datetime.timedelta(seconds=3600):
|
||||||
|
return T('n-minutes-ago').format(delta.seconds // 60)
|
||||||
|
|
||||||
|
elif delta < datetime.timedelta(days=1):
|
||||||
|
return T('n-hours-ago').format(delta.seconds // 3600)
|
||||||
|
elif delta < datetime.timedelta(days=15):
|
||||||
|
return T('n-days-ago').format(delta.days)
|
||||||
|
else:
|
||||||
|
return self.pub_date.strftime('%B %-d, %Y')
|
||||||
|
|
||||||
class PageTag(BaseModel):
|
class PageTag(BaseModel):
|
||||||
page = FK(Page, backref='tags', index=True)
|
page = FK(Page, backref='tags', index=True)
|
||||||
|
|
@ -496,13 +510,17 @@ forbidden_urls = [
|
||||||
'create', 'edit', 'p', 'ajax', 'history', 'manage', 'static', 'media',
|
'create', 'edit', 'p', 'ajax', 'history', 'manage', 'static', 'media',
|
||||||
'accounts', 'tags', 'init-config', 'upload', 'upload-info', 'about',
|
'accounts', 'tags', 'init-config', 'upload', 'upload-info', 'about',
|
||||||
'stats', 'terms', 'privacy', 'easter', 'search', 'help', 'circles',
|
'stats', 'terms', 'privacy', 'easter', 'search', 'help', 'circles',
|
||||||
'protect',
|
'protect', 'kt'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = 'qrdldCcvamtdcnidmtasegasdsedrdqvtautar'
|
app.secret_key = 'qrdldCcvamtdcnidmtasegasdsedrdqvtautar'
|
||||||
app.url_map.converters['slug'] = SlugConverter
|
app.url_map.converters['slug'] = SlugConverter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### ROUTES ####
|
#### ROUTES ####
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
|
|
@ -525,6 +543,12 @@ def _inject_variables():
|
||||||
'strong': lambda x:Markup('<strong>{0}</strong>').format(x),
|
'strong': lambda x:Markup('<strong>{0}</strong>').format(x),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.template_filter()
|
||||||
|
def linebreaks(text):
|
||||||
|
text = html.escape(text)
|
||||||
|
text = text.replace("\n\n", '</p><p>').replace('\n', '<br />')
|
||||||
|
return Markup(text)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def homepage():
|
def homepage():
|
||||||
page_limit = _getconf("appearance","items_per_page",20,cast=int)
|
page_limit = _getconf("appearance","items_per_page",20,cast=int)
|
||||||
|
|
@ -848,7 +872,7 @@ def easter_y(y=None):
|
||||||
|
|
||||||
#### EXTENSIONS ####
|
#### EXTENSIONS ####
|
||||||
|
|
||||||
active_extensions = ['circles']
|
active_extensions = ['contactnova']
|
||||||
|
|
||||||
for ext in active_extensions:
|
for ext in active_extensions:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
40
app_sync.py
40
app_sync.py
|
|
@ -68,6 +68,41 @@ def fetch_updated_ids(baseurl):
|
||||||
raise RuntimeError("sync unavailable")
|
raise RuntimeError("sync unavailable")
|
||||||
return r.json()["ids"]
|
return r.json()["ids"]
|
||||||
|
|
||||||
|
def update_contacts(baseurl):
|
||||||
|
from extensions.contactnova import Contact
|
||||||
|
try:
|
||||||
|
with open(_getconf("config", "database_dir") + "/latest_sync") as f:
|
||||||
|
last_sync = float(f.read().rstrip("\n"))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
last_sync = 946681200.0 # Jan 1, 2000
|
||||||
|
r = requests.get(baseurl + "/kt/_jsoninfo/{ts}".format(ts=last_sync))
|
||||||
|
if r.status_code >= 400:
|
||||||
|
raise RuntimeError("sync unavailable")
|
||||||
|
# update contacts
|
||||||
|
updated = 0
|
||||||
|
for pinfo in r.json()['data']:
|
||||||
|
p = Contact.get_or_none(Contact.code == pinfo['code'])
|
||||||
|
if p is None:
|
||||||
|
p = Contact.create(
|
||||||
|
code = pinfo['code'],
|
||||||
|
display_name = pinfo['display_name'],
|
||||||
|
issues = pinfo['issues'],
|
||||||
|
status = pinfo['status'],
|
||||||
|
description = pinfo['description'],
|
||||||
|
due = datetime.date.fromtimestamp(pinfo['due'])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
p.display_name = pinfo['display_name']
|
||||||
|
p.issues = pinfo['issues']
|
||||||
|
p.status = pinfo['status']
|
||||||
|
p.description = pinfo['description']
|
||||||
|
p.due = datetime.date.fromtimestamp(pinfo['due'])
|
||||||
|
p.touched = datetime.datetime.now()
|
||||||
|
p.save()
|
||||||
|
updated += 1
|
||||||
|
print('\x1b[32m{0} contacts updated :)\x1b[0m'.format(updated))
|
||||||
|
|
||||||
|
|
||||||
def update_page(p, pageinfo):
|
def update_page(p, pageinfo):
|
||||||
p.touched = datetime.datetime.fromtimestamp(pageinfo["touched"])
|
p.touched = datetime.datetime.fromtimestamp(pageinfo["touched"])
|
||||||
p.url = pageinfo["url"]
|
p.url = pageinfo["url"]
|
||||||
|
|
@ -123,6 +158,11 @@ def main():
|
||||||
if pageinfo["touched"] > p.touched.timestamp():
|
if pageinfo["touched"] > p.touched.timestamp():
|
||||||
update_page(p, pageinfo)
|
update_page(p, pageinfo)
|
||||||
passed += 1
|
passed += 1
|
||||||
|
try:
|
||||||
|
if _getconf("sync", "contacts", None) is not None:
|
||||||
|
update_contacts(baseurl)
|
||||||
|
except Exception as e:
|
||||||
|
print("\x1b[33mContacts not updated - {e} :(\x1b[0m".format(e=e))
|
||||||
with open(DATABASE_DIR + "/last_sync", "w") as fw:
|
with open(DATABASE_DIR + "/last_sync", "w") as fw:
|
||||||
fw.write(str(time.time()))
|
fw.write(str(time.time()))
|
||||||
if passed > 0 and failed == 0:
|
if passed > 0 and failed == 0:
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,8 @@ def edit_detail(id):
|
||||||
p.type = request.form["type"]
|
p.type = request.form["type"]
|
||||||
p.area = request.form['area']
|
p.area = request.form['area']
|
||||||
p.save()
|
p.save()
|
||||||
return redirect("/circles")
|
return redirect(request.form['returnto'])
|
||||||
return render_template("circles/add.html", pl=p)
|
return render_template("circles/add.html", pl=p, returnto=request.headers.get('Referer', '/circles'))
|
||||||
|
|
||||||
@bp.route('/csv', methods=['GET', 'POST'])
|
@bp.route('/csv', methods=['GET', 'POST'])
|
||||||
def add_csv():
|
def add_csv():
|
||||||
|
|
@ -207,6 +207,16 @@ def statuslist(typ):
|
||||||
q = Person.select().where(Person.status == typ).order_by(Person.touched.desc())
|
q = Person.select().where(Person.status == typ).order_by(Person.touched.desc())
|
||||||
return paginate_list(['Orange', 'Yellow', 'Green', ..., 'Red'][typ], q)
|
return paginate_list(['Orange', 'Yellow', 'Green', ..., 'Red'][typ], q)
|
||||||
|
|
||||||
|
@bp.route('/area-<int:a>')
|
||||||
|
def arealist(a):
|
||||||
|
q = Person.select().where(Person.area == a).order_by(Person.status.desc(), Person.touched.desc())
|
||||||
|
return paginate_list('Area {}'.format(a), q)
|
||||||
|
|
||||||
|
@bp.route('/no-area')
|
||||||
|
def noarealist():
|
||||||
|
q = Person.select().where(Person.area == 0).order_by(Person.touched.desc())
|
||||||
|
return paginate_list('Unassigned area', q)
|
||||||
|
|
||||||
@bp.route("/stats")
|
@bp.route("/stats")
|
||||||
def stats():
|
def stats():
|
||||||
bq = Person.select()
|
bq = Person.select()
|
||||||
|
|
@ -222,5 +232,10 @@ def stats():
|
||||||
'Orange': bq.where(Person.status == 0).count(),
|
'Orange': bq.where(Person.status == 0).count(),
|
||||||
'Yellow': bq.where(Person.status == 1).count(),
|
'Yellow': bq.where(Person.status == 1).count(),
|
||||||
'Green': bq.where(Person.status == 2).count()
|
'Green': bq.where(Person.status == 2).count()
|
||||||
}
|
},
|
||||||
|
area_count={
|
||||||
|
k: bq.where(Person.area == k).count()
|
||||||
|
for k in range(1, 13)
|
||||||
|
},
|
||||||
|
no_area_count=bq.where(Person.area == None).count()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
214
extensions/contactnova.py
Normal file
214
extensions/contactnova.py
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
# (c) 2021 Sakuragasaki46
|
||||||
|
# See LICENSE for copying info.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Contact Nova extension for Salvi.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from peewee import *
|
||||||
|
import datetime
|
||||||
|
from app import _getconf
|
||||||
|
from flask import Blueprint, request, redirect, render_template, jsonify, abort
|
||||||
|
from werkzeug.routing import BaseConverter
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
#### HELPERS ####
|
||||||
|
|
||||||
|
class RegExpField(CharField):
|
||||||
|
def __init__(self, regex, max_length=255, *args, **kw):
|
||||||
|
super().__init__(max_length, *args, **kw)
|
||||||
|
self.regex = regex
|
||||||
|
def db_value(self, value):
|
||||||
|
value = value.strip()
|
||||||
|
# XXX %: bug fix for LIKE, but it’s not something regular!
|
||||||
|
if not '%' in value and not re.fullmatch(self.regex, value):
|
||||||
|
raise IntegrityError("regexp mismatch: {!r}".format(self.regex))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def now7days():
|
||||||
|
return datetime.datetime.now() + datetime.timedelta(days=7)
|
||||||
|
|
||||||
|
#### DATABASE SCHEMA ####
|
||||||
|
|
||||||
|
database = SqliteDatabase(_getconf("config", "database_dir") + '/contactnova.sqlite')
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
class Meta:
|
||||||
|
database = database
|
||||||
|
|
||||||
|
ST_UNKNOWN = 0
|
||||||
|
ST_OK = 1
|
||||||
|
ST_ISSUES = 2
|
||||||
|
|
||||||
|
class Contact(BaseModel):
|
||||||
|
code = RegExpField(r'[A-Z]\.\d+', 30, unique=True)
|
||||||
|
display_name = RegExpField('[A-Za-z0-9_. ]+', 50, index=True)
|
||||||
|
status = IntegerField(default=ST_UNKNOWN, index=True)
|
||||||
|
issues = CharField(500, default='')
|
||||||
|
description = CharField(5000, default='')
|
||||||
|
due = DateField(index=True, default=now7days)
|
||||||
|
touched = DateTimeField(index=True, default=datetime.datetime.now)
|
||||||
|
def status_str(self):
|
||||||
|
return {
|
||||||
|
1: "task_alt",
|
||||||
|
2: "error_outline",
|
||||||
|
}.get(self.status, "")
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{0.__class__.__name__}: {0.code}>'.format(self)
|
||||||
|
@classmethod
|
||||||
|
def next_code(cls, letter):
|
||||||
|
try:
|
||||||
|
last_code = Contact.select().where(Contact.code ** (letter + '%')).order_by(Contact.id.desc()).first().code.split('.')
|
||||||
|
except (Contact.DoesNotExist, AttributeError):
|
||||||
|
last_code = letter, '0'
|
||||||
|
code = letter + '.' + str(int(last_code[1]) + 1)
|
||||||
|
return code
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
database.create_tables([Contact])
|
||||||
|
|
||||||
|
def create_cli():
|
||||||
|
code = Contact.next_code(input('Code letter:'))
|
||||||
|
|
||||||
|
return Contact.create(
|
||||||
|
code=code,
|
||||||
|
display_name=input('Display name:'),
|
||||||
|
due=datetime.datetime.fromisoformat(input('Due date (ISO):')),
|
||||||
|
description=input('Description (optional):')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Helper for command line.
|
||||||
|
class _CCClass:
|
||||||
|
def __init__(self, cb=lambda x:x):
|
||||||
|
self.callback = cb
|
||||||
|
def __neg__(self):
|
||||||
|
return create_cli()
|
||||||
|
def __getattr__(self, value):
|
||||||
|
code = value[0] + '.' + value[1:]
|
||||||
|
try:
|
||||||
|
return self.callback(Contact.get(Contact.code == code))
|
||||||
|
except Contact.DoesNotExist:
|
||||||
|
raise AttributeError(value) from None
|
||||||
|
def ok(self):
|
||||||
|
def cb(a):
|
||||||
|
a.status = 1
|
||||||
|
a.save()
|
||||||
|
return a
|
||||||
|
return self.__class__(cb)
|
||||||
|
CC = _CCClass()
|
||||||
|
del _CCClass
|
||||||
|
|
||||||
|
#### ROUTE HELPERS ####
|
||||||
|
|
||||||
|
class ContactCodeConverter(BaseConverter):
|
||||||
|
regex = r'[A-Z]\.\d+'
|
||||||
|
|
||||||
|
class ContactMockConverter(BaseConverter):
|
||||||
|
regex = r'[A-Z]'
|
||||||
|
|
||||||
|
def _register_converters(state):
|
||||||
|
state.app.url_map.converters['contactcode'] = ContactCodeConverter
|
||||||
|
state.app.url_map.converters['singleletter'] = ContactMockConverter
|
||||||
|
|
||||||
|
# Helper for pagination.
|
||||||
|
def paginate_list(cat, q):
|
||||||
|
pageno = int(request.args.get('page', 1))
|
||||||
|
return render_template(
|
||||||
|
"contactnova/list.html",
|
||||||
|
cat=cat,
|
||||||
|
count=q.count(),
|
||||||
|
people=q.offset(50 * (pageno - 1)).limit(50),
|
||||||
|
pageno=pageno
|
||||||
|
)
|
||||||
|
|
||||||
|
bp = Blueprint('contactnova', __name__,
|
||||||
|
url_prefix='/kt')
|
||||||
|
bp.record_once(_register_converters)
|
||||||
|
|
||||||
|
@bp.route('/init-config')
|
||||||
|
def _init_config():
|
||||||
|
init_db()
|
||||||
|
return redirect('/kt')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def homepage():
|
||||||
|
q = Contact.select().where(Contact.due > datetime.datetime.now()).order_by(Contact.due.desc())
|
||||||
|
return paginate_list('All', q)
|
||||||
|
|
||||||
|
@bp.route('/expired')
|
||||||
|
def expired():
|
||||||
|
q = Contact.select().where(Contact.due <= datetime.datetime.now()).order_by(Contact.due)
|
||||||
|
return paginate_list('Expired', q)
|
||||||
|
|
||||||
|
@bp.route('/ok')
|
||||||
|
def sanecontacts():
|
||||||
|
q = Contact.select().where((Contact.due > datetime.datetime.now()) &
|
||||||
|
(Contact.status == ST_OK)).order_by(Contact.due)
|
||||||
|
return paginate_list('Sane', q)
|
||||||
|
|
||||||
|
@bp.route('/<singleletter:l>')
|
||||||
|
def singleletter(l):
|
||||||
|
q = Contact.select().where(Contact.code ** (l + '%')).order_by(Contact.id)
|
||||||
|
return paginate_list('Series {}'.format(l), q)
|
||||||
|
|
||||||
|
@bp.route('/<contactcode:code>')
|
||||||
|
def singlecontact(code):
|
||||||
|
try:
|
||||||
|
p = Contact.get(Contact.code == code)
|
||||||
|
except IntegrityError:
|
||||||
|
abort(404)
|
||||||
|
return render_template('contactnova/single.html', p=p)
|
||||||
|
|
||||||
|
@bp.route('/_newcode/<singleletter:l>')
|
||||||
|
def newcode(l):
|
||||||
|
return Contact.next_code(l), {"Content-Type": "text/plain"}
|
||||||
|
|
||||||
|
@bp.route('/new', methods=['GET', 'POST'])
|
||||||
|
def newcontact():
|
||||||
|
if request.method == 'POST':
|
||||||
|
Contact.create(
|
||||||
|
code = Contact.next_code(request.form['letter']),
|
||||||
|
display_name = request.form['display_name'],
|
||||||
|
status = request.form['status'],
|
||||||
|
issues = request.form['issues'],
|
||||||
|
description = request.form['description'],
|
||||||
|
due = datetime.date.fromisoformat(request.form['due'])
|
||||||
|
)
|
||||||
|
return redirect(request.form.get('returnto','/kt/'+request.form['letter']))
|
||||||
|
return render_template('contactnova/new.html', pl_date = now7days())
|
||||||
|
|
||||||
|
@bp.route('/edit/<contactcode:code>', methods=['GET', 'POST'])
|
||||||
|
def editcontact(code):
|
||||||
|
pl = Contact.get(Contact.code == code)
|
||||||
|
if request.method == 'POST':
|
||||||
|
pl.display_name = request.form['display_name']
|
||||||
|
pl.issues = request.form['issues']
|
||||||
|
pl.status = request.form['status']
|
||||||
|
pl.description = request.form['description']
|
||||||
|
pl.due = datetime.date.fromisoformat(request.form['due'])
|
||||||
|
pl.touched = datetime.datetime.now()
|
||||||
|
pl.save()
|
||||||
|
return redirect(request.form.get('returnto','/kt/'+pl.code[0]))
|
||||||
|
return render_template('contactnova/new.html', pl = pl)
|
||||||
|
|
||||||
|
@bp.route('/_jsoninfo/<float:ts>')
|
||||||
|
def contact_jsoninfo(ts):
|
||||||
|
tse = str(datetime.datetime.fromtimestamp(ts).isoformat(" "))
|
||||||
|
ps = Contact.select().where(Contact.touched >= tse)
|
||||||
|
return jsonify({
|
||||||
|
"ids": [i.code for i in ps],
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
'code': i.code,
|
||||||
|
'display_name': i.display_name,
|
||||||
|
'due': datetime.datetime.fromisoformat(i.due.isoformat()).timestamp(),
|
||||||
|
'issues': i.issues,
|
||||||
|
'description': i.description,
|
||||||
|
'status': i.status
|
||||||
|
} for i in ps
|
||||||
|
],
|
||||||
|
"status": "ok"
|
||||||
|
})
|
||||||
|
|
@ -41,6 +41,8 @@ input[type="submit"],input[type="reset"],input[type="button"],button{font-family
|
||||||
.page-tags .tag-count{color:#3c3;font-size:smaller;font-weight:600}
|
.page-tags .tag-count{color:#3c3;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}
|
||||||
|
|
||||||
|
/* Circles extension */
|
||||||
.circles-red{color: #e14}
|
.circles-red{color: #e14}
|
||||||
.circles-orange{color: #f72}
|
.circles-orange{color: #f72}
|
||||||
.circles-green{color: #6e4}
|
.circles-green{color: #6e4}
|
||||||
|
|
@ -56,6 +58,14 @@ input[type="submit"],input[type="reset"],input[type="button"],button{font-family
|
||||||
.circles-add-form > div{display:table-row}
|
.circles-add-form > div{display:table-row}
|
||||||
.circles-add-form > div > *{display:table-cell}
|
.circles-add-form > div > *{display:table-cell}
|
||||||
.circles-add-form > div > label{text-align:right}
|
.circles-add-form > div > label{text-align:right}
|
||||||
|
.circles-li-code {font-size: 120%}
|
||||||
|
|
||||||
|
/* ContactNova extension */
|
||||||
|
.contactnova-issues {padding-left: 12px; border-left: 4px solid #fc6}
|
||||||
|
.contactnova-col-code {font-size: 1.25em; font-weight: bold}
|
||||||
|
.contactnova-status_1 .material-icons {color: #6e4}
|
||||||
|
.contactnova-status_2 .material-icons {color: #fc6}
|
||||||
|
.contactnova-list td {vertical-align: middle}
|
||||||
|
|
||||||
|
|
||||||
/* floating elements */
|
/* floating elements */
|
||||||
|
|
@ -132,3 +142,19 @@ body.dark, .dark input, .dark textarea{background-color: #1f1f1f; color: white}
|
||||||
a.dark-theme-toggle-off{display: none}
|
a.dark-theme-toggle-off{display: none}
|
||||||
.dark a.dark-theme-toggle-off{display: inline}
|
.dark a.dark-theme-toggle-off{display: inline}
|
||||||
.dark a.dark-theme-toggle-on{display: none}
|
.dark a.dark-theme-toggle-on{display: none}
|
||||||
|
|
||||||
|
/* ?????? */
|
||||||
|
.wrap_responsive_cells {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
/* This is better for small screens, once min() is better supported */
|
||||||
|
/* grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); */
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.wrap_responsive_cells_narrow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
/* This is better for small screens, once min() is better supported */
|
||||||
|
/* grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); */
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,7 @@ search,Search,Cerca
|
||||||
year,Year,Anno
|
year,Year,Anno
|
||||||
calculate,Calculate,Calcola
|
calculate,Calculate,Calcola
|
||||||
show-all,Show all,Mostra tutto
|
show-all,Show all,Mostra tutto
|
||||||
|
just-now,just now,poco fa
|
||||||
|
n-minutes-ago,{0} minutes ago,{0} minuti fa
|
||||||
|
n-hours-ago,{0} hours ago,{0} ore fa
|
||||||
|
n-days-ago,{0} days ago,{0} giorni fa
|
||||||
|
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<form method="POST" class="circles-add-form">
|
<form method="POST" class="circles-add-form">
|
||||||
|
{% if returnto %}<input type="hidden" name="returnto" value="{{ returnto }}">{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<label>#</label>
|
<label>#</label>
|
||||||
<input type="number" name="code" placeholder="00000" required="" maxlength="6" value="{{ pl.code if pl else '' }}" />
|
<input type="number" name="code" placeholder="00000" required="" maxlength="6" value="{{ pl.code if pl else '' }}" />
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Showing: <strong>{{ cat }}</strong></p>
|
<p>Showing: <strong>{{ cat }}</strong></p>
|
||||||
|
|
||||||
<div>
|
<fieldset>
|
||||||
<p>Show by:</p>
|
<legend>Show by:</legend>
|
||||||
<p><strong>Type</strong>:
|
<p><strong>Type</strong>:
|
||||||
{% set typ_list = [
|
{% set typ_list = [
|
||||||
'INTJ', 'INTP', 'INFJ', 'INFP', 'ENTJ', 'ENTP',
|
'INTJ', 'INTP', 'INFJ', 'INFP', 'ENTJ', 'ENTP',
|
||||||
|
|
@ -26,7 +26,9 @@
|
||||||
{% for t in range(1, 13) %}
|
{% for t in range(1, 13) %}
|
||||||
<a href="/circles/area-{{ t }}">{{ t }}</a> ·
|
<a href="/circles/area-{{ t }}">{{ t }}</a> ·
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<a href="/circles/no-area">Unassigned</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p><a href="/circles/stats">Stats</a> · <a href="/circles/new">Add new</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if count > people.count() %}
|
{% if count > people.count() %}
|
||||||
|
|
@ -42,7 +44,10 @@
|
||||||
|
|
||||||
<ul class="circles-list">
|
<ul class="circles-list">
|
||||||
{% for p in people %}
|
{% for p in people %}
|
||||||
<li class="circles-{{ 'red' if p.status == -1 else 'green' if p.status == 2 else 'yellow' if p.status == 1 else 'orange' }}">{{ p.code }} – {{ p.display_name }} – {{ p.type }} – {% if p.area %}Area {{ p.area }}{% else %}No area{% endif %} (<a href="/circles/edit/{{ p.code }}">edit</a>)</li>
|
<li class="circles-{{ 'red' if p.status == -1 else 'green' if p.status == 2 else 'yellow' if p.status == 1 else 'orange' }}">
|
||||||
|
<span class="circles-li-code">{{ p.code }}</span>
|
||||||
|
<span>{{ p.display_name }} – {{ p.type }} – {% if p.area %}Area {{ p.area }}{% else %}No area{% endif %} (<a href="/circles/edit/{{ p.code }}">edit</a>)</span>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>All people: <strong>{{ count }}</strong></li>
|
<li>All people: <strong>{{ count }}</strong></li>
|
||||||
<li>People by type:</li>
|
<li>People by type:</li>
|
||||||
<ul>
|
<ul class="wrap_responsive_cells_narrow">
|
||||||
{% for k in typed_count.items() %}
|
{% for k in typed_count.items() %}
|
||||||
<li>{{ k[0] }}: <strong>{{ k[1] }}</strong></li>
|
<li>{{ k[0] }}: <strong>{{ k[1] }}</strong></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -19,5 +19,12 @@
|
||||||
<li>{{ k[0] }}: <strong>{{ k[1] }}</strong></li>
|
<li>{{ k[0] }}: <strong>{{ k[1] }}</strong></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<li>People by area:</li>
|
||||||
|
<ul class="wrap_responsive_cells_narrow">
|
||||||
|
{% for k in area_count.items() %}
|
||||||
|
<li>Area {{ k[0] }}: <strong>{{ k[1] }}</strong></li>
|
||||||
|
{% endfor %}
|
||||||
|
<li>No area: <strong>{{ no_area_count }}</strong></li>
|
||||||
|
</ul>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
59
templates/contactnova/list.html
Normal file
59
templates/contactnova/list.html
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}List of contacts – {{ app_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Showing: <strong>{{ cat }}</strong> · <a href="/kt/new">New contact</a></p>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Show by:</legend>
|
||||||
|
<p><strong>Letter</strong>:
|
||||||
|
{% set typ_list = 'ABCDEFGHIJKLMNOPQRSTUVWYZ' %}
|
||||||
|
{% for t in typ_list %}
|
||||||
|
<a href="/kt/{{ t }}">{{ t }}</a> ·
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="/kt/">All</a> ·
|
||||||
|
<a href="/kt/expired">Expired</a> ·
|
||||||
|
<a href="/kt/ok">Sane</a>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{% if count > people.count() %}
|
||||||
|
<p>Showing <strong>{{ people.count() }}</strong> people of <strong>{{ count }}</strong> total.</p>
|
||||||
|
{% if count > pageno * 50 %}
|
||||||
|
<p><a href="?page={{ pageno + 1 }}" rel="nofollow">Next page</a>{% if pageno > 1 %} · <a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a>{% endif %}</p>
|
||||||
|
{% elif pageno > 1 %}
|
||||||
|
<a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p><strong>{{ count }}</strong> people.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="contactnova-list">
|
||||||
|
<thead>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in people %}
|
||||||
|
<tr class="contactnova-status_{{ p.status }}">
|
||||||
|
<td><span class="material-icons">{{ p.status_str() }}</span></td>
|
||||||
|
<td class="contactnova-col-code"><a href="/kt/{{ p.code }}">{{ p.code }}</a></td>
|
||||||
|
<td>{{ p.display_name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if count > people.count() %}
|
||||||
|
<p>Showing <strong>{{ people.count() }}</strong> people of <strong>{{ count }}</strong> total.</p>
|
||||||
|
{% if count > pageno * 50 %}
|
||||||
|
<p><a href="?page={{ pageno + 1 }}" rel="nofollow">Next page</a>{% if pageno > 1 %} · <a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a>{% endif %}</p>
|
||||||
|
{% elif pageno > 1 %}
|
||||||
|
<a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p><strong>{{ count }}</strong> people.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
72
templates/contactnova/new.html
Normal file
72
templates/contactnova/new.html
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Create new contact – {{ app_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" class="circles-add-form">
|
||||||
|
{% if returnto %}<input type="hidden" name="returnto" value="{{ returnto }}">{% endif %}
|
||||||
|
{% if not pl %}
|
||||||
|
<div>
|
||||||
|
<label>Letter</label>
|
||||||
|
<select id="ktCodeLetter" name="letter">
|
||||||
|
<option value="-" disabled="" selected="">(Choose)</option>
|
||||||
|
{% set typ_list = 'ABCDEFGHIJKLMNOPQRSTUVWYZ' %}
|
||||||
|
{% for t in typ_list %}
|
||||||
|
<option value="{{ t }}">{{ t }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<label>Code</label>
|
||||||
|
<strong id="ktNewCode">---</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Display name</label>
|
||||||
|
<input name="display_name" maxlength="50"{% if pl %} value="{{ pl.display_name }}"{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Status</label>
|
||||||
|
<select name="status">
|
||||||
|
{% set statuses = {
|
||||||
|
0: 'Variable',
|
||||||
|
1: 'OK',
|
||||||
|
2: 'Issues',
|
||||||
|
} %}
|
||||||
|
{% for k, v in statuses.items() %}
|
||||||
|
<option value="{{ k }}"{% if pl and pl.status == k %} selected=""{% endif %}>{{ v }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Issues</label>
|
||||||
|
<textarea maxlength="500" name="issues">{{ pl and pl.issues }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea maxlength="5000" name="description">{{ pl and pl.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Due</label>
|
||||||
|
<input name="due" required="" type="date" value="{{ pl.due if pl else pl_date }}" min="2020-01-01" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" id="ktSubmit" value="Save">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{% if not pl %}
|
||||||
|
ktSubmit.disabled = true;
|
||||||
|
{% endif %}
|
||||||
|
ktCodeLetter.onchange = function(){
|
||||||
|
let x = new XMLHttpRequest;
|
||||||
|
x.open('GET', '/kt/_newcode/' + ktCodeLetter.value);
|
||||||
|
x.onreadystatechange = () => {
|
||||||
|
if (x.readyState === XMLHttpRequest.DONE && x.status == 200) {
|
||||||
|
ktNewCode.textContent = x.responseText;
|
||||||
|
ktSubmit.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
x.send();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
24
templates/contactnova/single.html
Normal file
24
templates/contactnova/single.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Contact {{ p.code }} – {{ app_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ p.code }}</h1>
|
||||||
|
|
||||||
|
<p>aka: <strong>{{ p.display_name }}</strong></p>
|
||||||
|
|
||||||
|
{% if p.issues %}
|
||||||
|
<p class="contactnova-issues">{{ p.issues }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="contactnova-description">
|
||||||
|
{{ p.description or "No description available." |linebreaks }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
<p>Expires: {{ p.due.strftime('%B %-d, %Y') }}</p>
|
||||||
|
|
||||||
|
<p><a href="/kt/">Back</a> · <a href="/kt/edit/{{ p.code }}?returnto=/kt/{{ p.code }}">Edit contact</a></p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ p.title }}</h1>
|
<h1>{{ p.title }}</h1>
|
||||||
|
|
||||||
<div class="jump-to-actions"><a href="#page-actions">{{ T('jump-to-actions') }}</a></div>
|
<div class="jump-to-actions">
|
||||||
|
<span>{{ T('last-changed') }} {{ rev.human_pub_date() }}</span> ·
|
||||||
|
<a href="#page-actions">{{ T('jump-to-actions') }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block history_nav %}{% endblock %}
|
{% block history_nav %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue