Adding some extensions and appearance improvements.
This commit is contained in:
parent
ec720743b3
commit
479d8eecc0
13 changed files with 506 additions and 12 deletions
|
|
@ -170,8 +170,8 @@ def edit_detail(id):
|
|||
p.type = request.form["type"]
|
||||
p.area = request.form['area']
|
||||
p.save()
|
||||
return redirect("/circles")
|
||||
return render_template("circles/add.html", pl=p)
|
||||
return redirect(request.form['returnto'])
|
||||
return render_template("circles/add.html", pl=p, returnto=request.headers.get('Referer', '/circles'))
|
||||
|
||||
@bp.route('/csv', methods=['GET', 'POST'])
|
||||
def add_csv():
|
||||
|
|
@ -207,6 +207,16 @@ def statuslist(typ):
|
|||
q = Person.select().where(Person.status == typ).order_by(Person.touched.desc())
|
||||
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")
|
||||
def stats():
|
||||
bq = Person.select()
|
||||
|
|
@ -222,5 +232,10 @@ def stats():
|
|||
'Orange': bq.where(Person.status == 0).count(),
|
||||
'Yellow': bq.where(Person.status == 1).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"
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue