diff --git a/app.py b/app.py index c95cd47..9615406 100644 --- a/app.py +++ b/app.py @@ -848,7 +848,7 @@ def easter_y(y=None): #### EXTENSIONS #### -active_extensions = [] +active_extensions = ['circles'] for ext in active_extensions: try: diff --git a/extensions/__init__.py b/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extensions/circles.py b/extensions/circles.py index 5b2993e..9ddbc05 100644 --- a/extensions/circles.py +++ b/extensions/circles.py @@ -6,7 +6,13 @@ Circles (people) extension for Salvi. ''' from peewee import * -from ..app import _getconf +import datetime +from app import _getconf +from flask import Blueprint, request, redirect, render_template +from werkzeug.routing import BaseConverter +import csv +import io +import itertools #### HELPERS #### @@ -25,7 +31,7 @@ def _getmbt(s): def _putmbt(s): if s & 16 == 0: - return "1x38b" + return "1x38B" return ''.join(( 'IE'[(s & 8) >> 3], 'NS'[(s & 4) >> 2], @@ -34,6 +40,7 @@ def _putmbt(s): )) + #### DATABASE SCHEMA #### database = SqliteDatabase(_getconf("config", "database_dir") + '/circles.sqlite') @@ -46,10 +53,12 @@ class MbTypeField(Field): field_type = 'integer' def db_value(self, value): + if isinstance(value, int): + return value return _getmbt(value) def python_value(self, value): return _putmbt(value) - + ST_ORANGE = 0 ST_YELLOW = 1 ST_GREEN = 2 @@ -63,6 +72,8 @@ class Person(BaseModel): circle = IntegerField(default=7, index=True) status = IntegerField(default=ST_ORANGE, index=True) type = MbTypeField(default=0, index=True) + area = IntegerField(default=0, index=True) + touched = DateTimeField(default=datetime.datetime.now) class Meta: indexes = ( (('last_name', 'first_name'), False), @@ -71,16 +82,145 @@ class Person(BaseModel): def init_db(): database.create_tables([Person]) +def add_from_csv(s): + f = io.StringIO() + f.write(s) + f.seek(0) + rd = csv.reader(f) + for line in rd: + code = line[0] + if not code.isdigit(): + continue + names = line[1:4] + while len(names) < 3: + names.append('') + if not names[2]: + names[2] = names[0] + " " + names[1] + type_ = line[4] if len(line) > 4 else 0 + try: + p = Person[code] + except Person.DoesNotExist: + p = Person.create( + code = code, + display_name = names[2], + first_name = names[0], + last_name = names[1], + type = type_ + ) + else: + p.touched = datetime.datetime.now() + p.first_name, p.last_name, p.display_name = names + p.type = type_ or p.type + p.save() + #### ROUTING #### +class MbTypeConverter(BaseConverter): + regex = '[IE][NS][TF][JP]|[ie][ns][tf][jp]' + def to_python(self, value): + return value.upper() + def to_url(self, value): + return value.lower() + +class StatusColorConverter(BaseConverter): + regex = 'red|yellow|green|orange' + def to_python(self, value): + if value == 'red': + return -1 + return ['orange', 'yellow', 'green'].index(value) + def to_url(self, value): + return ['orange', 'yellow', 'green', ..., 'red'][value] + +def _register_converters(state): + state.app.url_map.converters['mbtype'] = MbTypeConverter + state.app.url_map.converters['statuscolor'] = StatusColorConverter + bp = Blueprint('circles', __name__, url_prefix='/circles') +bp.record_once(_register_converters) @bp.route('/init-config') def _init_config(): init_db() return redirect('/circles') +@bp.route('/new', methods=['GET', 'POST']) +def add_new(): + if request.method == 'POST': + p = Person.create( + code = request.form["code"], + display_name = request.form["display_name"], + first_name = request.form["first_name"], + last_name = request.form["last_name"], + type = request.form["type"], + status = int(request.form.get('status', 0)) + ) + return redirect("/circles") + return render_template("circles/add.html") + +@bp.route('/edit/', methods=['GET', 'POST']) +def edit_detail(id): + p = Person[id] + if request.method == 'POST': + p.touched = datetime.datetime.now() + p.first_name = request.form['first_name'] + p.last_name = request.form['last_name'] + p.display_name = request.form['display_name'] + p.status = int(request.form.get('status', 0)) + p.type = request.form["type"] + p.area = request.form['area'] + p.save() + return redirect("/circles") + return render_template("circles/add.html", pl=p) + +@bp.route('/csv', methods=['GET', 'POST']) +def add_csv(): + if request.method == 'POST' and request.form.get('consent') == 'y': + add_from_csv(request.form['text']) + return redirect('/circles') + return render_template("circles/csv.html") + +# Helper for pagination. +def paginate_list(cat, q): + pageno = int(request.args.get('page', 1)) + return render_template( + "circles/list.html", + cat=cat, + count=q.count(), + people=q.offset(50 * (pageno - 1)).limit(50), + pageno=pageno + ) + @bp.route('/') def homepage(): - return render_template("base.html") + q = Person.select().order_by(Person.touched.desc()) + return paginate_list('all', q) + + +@bp.route('/') +def typelist(typ): + q = Person.select().where(Person.type == typ).order_by(Person.touched.desc()) + return paginate_list(typ, q) + +@bp.route('/') +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("/stats") +def stats(): + bq = Person.select() + return render_template( + "circles/stats.html", + count=bq.count(), + typed_count={ + ''.join(x) : bq.where(Person.type == ''.join(x)).count() + for x in itertools.product('IE', 'NS', 'TF', 'JP') + }, + status_count={ + 'Red': bq.where(Person.status == -1).count(), + 'Orange': bq.where(Person.status == 0).count(), + 'Yellow': bq.where(Person.status == 1).count(), + 'Green': bq.where(Person.status == 2).count() + } + ) diff --git a/static/style.css b/static/style.css index 58d7faa..19d3ae5 100644 --- a/static/style.css +++ b/static/style.css @@ -41,6 +41,21 @@ input[type="submit"],input[type="reset"],input[type="button"],button{font-family .page-tags .tag-count{color:#3c3;font-size:smaller;font-weight:600} .search-wrapper {display:flex;width:90%;margin:auto} .search-wrapper > input {flex:1} +.circles-red{color: #e14} +.circles-orange{color: #f72} +.circles-green{color: #6e4} +.circles-yellow{color: #fc6} +.circles-list{list-style: none} +.circles-list > li{margin: .5em 0;} +.circles-list > li::before{font-size: 24px;transform:translatey(50%);font-weight:bold;margin-right:8px} +.circles-list > li.circles-red::before{color: #e14; content: '❌︎'} +.circles-list > li.circles-orange::before{color: #f72; content: '△'} +.circles-list > li.circles-yellow::before{color: #fc6; content: '◇'} +.circles-list > li.circles-green::before{color: #6e4; content: '○'} +.circles-add-form{display:table} +.circles-add-form > div{display:table-row} +.circles-add-form > div > *{display:table-cell} +.circles-add-form > div > label{text-align:right} /* floating elements */ diff --git a/templates/circles/add.html b/templates/circles/add.html new file mode 100644 index 0000000..de1c79c --- /dev/null +++ b/templates/circles/add.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}Circles – {{ app_name }}{% endblock %} + +{% block content %} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +{% if not pl %} +

Looking for mass addition? Try CSV adding form instead.

+{% endif %} + +{% endblock %} diff --git a/templates/circles/csv.html b/templates/circles/csv.html new file mode 100644 index 0000000..e50404c --- /dev/null +++ b/templates/circles/csv.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}Circles – {{ app_name }}{% endblock %} + +{% block content %} + +
+

Enter the contacts you want to bulk add, in comma-separated values (CSV) format, one by line.
Order matters.

+ + + +
+ + +{% endblock %} diff --git a/templates/circles/list.html b/templates/circles/list.html new file mode 100644 index 0000000..cb334ae --- /dev/null +++ b/templates/circles/list.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} + +{% block title %}Circles – {{ app_name }}{% endblock %} + +{% block content %} +

Showing: {{ cat }}

+ +
+

Show by:

+

Type: + {% set typ_list = [ + 'INTJ', 'INTP', 'INFJ', 'INFP', 'ENTJ', 'ENTP', + 'ENFJ', 'ENFP', 'ISTJ', 'ISTP', 'ISFJ', 'ISFP', + 'ESTJ', 'ESTP', 'ESFJ', 'ESFP'] %} + {% for t in typ_list %} + {{ t }} · + {% endfor %} +

+

Status: + {% set typ_list = ['Green', 'Yellow', 'Orange', 'Red'] %} + {% for t in typ_list %} + {{ t }} · + {% endfor %} +

+

Area: + {% for t in range(1, 13) %} + {{ t }} · + {% endfor %} +

+
+ +{% if count > people.count() %} +

Showing {{ people.count() }} people of {{ count }} total.

+{% if count > pageno * 50 %} +

Next page{% if pageno > 1 %} · Prev page{% endif %}

+{% elif pageno > 1 %} +Prev page

+{% endif %} +{% else %} +

{{ count }} people.

+{% endif %} + +
    + {% for p in people %} +
  • {{ p.code }} – {{ p.display_name }} – {{ p.type }} – {% if p.area %}Area {{ p.area }}{% else %}No area{% endif %} (edit)
  • + {% endfor %} +
+ +{% if count > people.count() %} +

Showing {{ people.count() }} people of {{ count }} total.

+{% if count > pageno * 50 %} +

Next page{% if pageno > 1 %} · Prev page{% endif %}

+{% elif pageno > 1 %} +Prev page

+{% endif %} +{% else %} +

{{ count }} people.

+{% endif %} + +{% endblock %} diff --git a/templates/circles/stats.html b/templates/circles/stats.html new file mode 100644 index 0000000..43cfa2e --- /dev/null +++ b/templates/circles/stats.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}Circles – {{ app_name }}{% endblock %} + +{% block content %} +

Stats

+ +
    +
  • All people: {{ count }}
  • +
  • People by type:
  • +
      + {% for k in typed_count.items() %} +
    • {{ k[0] }}: {{ k[1] }}
    • + {% endfor %} +
    +
  • People by status zone:
  • +
      + {% for k in status_count.items() %} +
    • {{ k[0] }}: {{ k[1] }}
    • + {% endfor %} +
    +
+{% endblock %}