forked from k-space/inventory-app
Compare commits
1 Commits
master
...
issue-7-ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692438ea2b |
@@ -8,7 +8,7 @@ from flask_wtf import FlaskForm
|
|||||||
from oidc import login_required, read_user
|
from oidc import login_required, read_user
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
|
from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired, InputRequired
|
||||||
|
|
||||||
page_doorboy = Blueprint("doorboy", __name__)
|
page_doorboy = Blueprint("doorboy", __name__)
|
||||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||||
@@ -82,9 +82,19 @@ def save_doorboy_edit(token_id):
|
|||||||
})
|
})
|
||||||
return redirect("/m/doorboy/me")
|
return redirect("/m/doorboy/me")
|
||||||
|
|
||||||
|
def hold_duration_check(form, field):
|
||||||
|
# 0 cancels/closes an active hold immediately; any real hold must last at
|
||||||
|
# least the documented 5-second minimum (and at most 6 hours). Reject 1-4
|
||||||
|
# so a typo cannot arm an unusably short hold. field.data is None when the
|
||||||
|
# submitted value was not a valid integer; leave that to the other validators.
|
||||||
|
if field.data is None:
|
||||||
|
return
|
||||||
|
if field.data != 0 and not (5 <= field.data <= 21600):
|
||||||
|
raise validators.ValidationError("Duration must be 0 (to close now) or between 5 and 21600 seconds")
|
||||||
|
|
||||||
class HoldDoorForm(FlaskForm):
|
class HoldDoorForm(FlaskForm):
|
||||||
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()])
|
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor", "workshopdoor"]], validators=[DataRequired()])
|
||||||
duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)])
|
duration = IntegerField('Duration in seconds', validators=[InputRequired(), hold_duration_check])
|
||||||
|
|
||||||
# duration=0 to override and close right away
|
# duration=0 to override and close right away
|
||||||
@page_doorboy.route("/m/doorboy/hold", methods=["POST"])
|
@page_doorboy.route("/m/doorboy/hold", methods=["POST"])
|
||||||
@@ -92,16 +102,32 @@ class HoldDoorForm(FlaskForm):
|
|||||||
def view_doorboy_hold():
|
def view_doorboy_hold():
|
||||||
user = read_user()
|
user = read_user()
|
||||||
form = HoldDoorForm(request.form)
|
form = HoldDoorForm(request.form)
|
||||||
if form.validate_on_submit():
|
if not form.validate_on_submit():
|
||||||
|
# Validation or CSRF failure: report it instead of redirecting as if the
|
||||||
|
# hold had been accepted, so the user knows the door will not be held.
|
||||||
|
errors = "; ".join(m for messages in form.errors.values() for m in messages)
|
||||||
|
return "Invalid hold request: %s" % (errors or "bad or expired form"), 400
|
||||||
|
|
||||||
|
door = form.door_name.data
|
||||||
|
if door == "workshopdoor":
|
||||||
|
access_group = "k-space:workshop"
|
||||||
|
else:
|
||||||
|
access_group = "k-space:floor"
|
||||||
|
approved = access_group in g.users_lookup.get(user["username"], User()).groups
|
||||||
db.doorlog.insert_one({
|
db.doorlog.insert_one({
|
||||||
"method": "hold",
|
"method": "hold",
|
||||||
"timestamp": datetime.utcnow(),
|
"timestamp": datetime.utcnow(),
|
||||||
"door": form.door_name.data,
|
"door": door,
|
||||||
"approved": True,
|
"approved": approved,
|
||||||
"user": user["username"],
|
"user": user["username"],
|
||||||
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data)
|
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if approved:
|
||||||
return redirect("/m/doorboy")
|
return redirect("/m/doorboy")
|
||||||
|
# Logged for audit, but the proxy only honors approved holds, so tell the
|
||||||
|
# user the door will not actually be held rather than redirecting silently.
|
||||||
|
return "You are not in the access group required to hold this door", 403
|
||||||
|
|
||||||
# Writes open event to log, which is picked up by doorboy-proxy.
|
# Writes open event to log, which is picked up by doorboy-proxy.
|
||||||
@page_doorboy.route("/m/doorboy/<door>/open")
|
@page_doorboy.route("/m/doorboy/<door>/open")
|
||||||
@@ -135,6 +161,7 @@ def view_doorboy():
|
|||||||
user = read_user()
|
user = read_user()
|
||||||
workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups
|
workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups
|
||||||
|
|
||||||
|
hold_form = HoldDoorForm()
|
||||||
latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10)
|
latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10)
|
||||||
return render_template("doorboy.html", **locals())
|
return render_template("doorboy.html", **locals())
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ mongodb = mongoclient.get_default_database()
|
|||||||
mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True)
|
mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True)
|
||||||
mongodb.inventory.create_index("token.uid_hash", sparse=True, unique=True)
|
mongodb.inventory.create_index("token.uid_hash", sparse=True, unique=True)
|
||||||
#mongodb.inventory.create_index("token.uid_hash", unique=True)
|
#mongodb.inventory.create_index("token.uid_hash", unique=True)
|
||||||
|
# Supports doorboy-proxy's per-poll "newest active hold for this door" lookup
|
||||||
|
# (find_one({method,door,approved}, sort=timestamp desc)) without a collection scan.
|
||||||
|
# method is the leading equality field so the common no-active-hold poll does not
|
||||||
|
# scan every approved event for the door; timestamp trails for the descending sort.
|
||||||
|
mongodb.doorlog.create_index([("method", 1), ("door", 1), ("approved", 1), ("timestamp", -1)])
|
||||||
|
|
||||||
CATEGORY_COLORS = (
|
CATEGORY_COLORS = (
|
||||||
('membership-fee', '#acc236'),
|
('membership-fee', '#acc236'),
|
||||||
|
|||||||
@@ -37,6 +37,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Hold door open</h3>
|
||||||
|
<form method="POST" action="/m/doorboy/hold" autocomplete="off">
|
||||||
|
{{ hold_form.csrf_token }}
|
||||||
|
<select name="door_name">
|
||||||
|
<option value="grounddoor">Ground door</option>
|
||||||
|
<option value="frontdoor">Front door</option>
|
||||||
|
<option value="backdoor">Back door</option>
|
||||||
|
{% if workshop_access %}<option value="workshopdoor">Workshop</option>{% endif %}
|
||||||
|
</select>
|
||||||
|
<input type="number" name="duration" min="0" max="21600" value="300">
|
||||||
|
<button class="waves-effect waves-light btn" type="submit">Hold</button>
|
||||||
|
<p>Duration in seconds: 300 = 5 min, 3600 = 1h, max 21600 = 6h, 0 = cancel/close now.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div><ul>
|
<div><ul>
|
||||||
<li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li>
|
<li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li>
|
||||||
|
|||||||
Reference in New Issue
Block a user