Skip to content

Commit c41573d

Browse files
authored
In-Line Housing Update Functionality (#109)
* Add mass update endpoint * Add modal and update support * Change DOM instead of Reloading * Add new rooms to DOM * Added correct exception handling * Invert conditional to fix linter error
1 parent 1330435 commit c41573d

File tree

7 files changed

+331
-9
lines changed

7 files changed

+331
-9
lines changed

conditional/blueprints/housing.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
from conditional.models.models import FreshmanAccount
77
from conditional.models.models import InHousingQueue
88
from conditional.util.housing import get_housing_queue
9-
from conditional.util.ldap import ldap_get_onfloor_members, ldap_is_eval_director, ldap_get_member
10-
from conditional.util.flask import render_template
11-
9+
from conditional.util.ldap import ldap_get_onfloor_members
10+
from conditional.util.ldap import ldap_is_eval_director
11+
from conditional.util.ldap import ldap_get_member
1212
from conditional.util.ldap import ldap_get_roomnumber
13+
from conditional.util.ldap import ldap_get_current_students
14+
15+
from conditional.util.flask import render_template
1316

1417
from conditional import db
1518

@@ -90,3 +93,48 @@ def change_queue_state():
9093
db.session.flush()
9194
db.session.commit()
9295
return jsonify({"success": True}), 200
96+
97+
98+
@housing_bp.route('/housing/update/<rmnumber>', methods=['POST'])
99+
def change_room_numbers(rmnumber):
100+
log = logger.new(user_name=request.headers.get("x-webauth-user"),
101+
request_id=str(uuid.uuid4()))
102+
log.info('api', action='mass housing update')
103+
104+
username = request.headers.get('x-webauth-user')
105+
account = ldap_get_member(username)
106+
update = request.get_json()
107+
108+
if not ldap_is_eval_director(account):
109+
return "must be eval director", 403
110+
111+
# Get the current list of people living on-floor.
112+
current_students = ldap_get_current_students()
113+
114+
# Set the new room number for each person in the list.
115+
116+
for occupant in update["occupants"]:
117+
if occupant != "":
118+
account = ldap_get_member(occupant)
119+
account.roomNumber = rmnumber
120+
log.info('api', action='%s assigned to room %s' % (occupant, rmnumber))
121+
# Delete any old occupants that are no longer in room.
122+
for old_occupant in [account for account in current_students
123+
if ldap_get_roomnumber(account) == str(rmnumber)
124+
and account.uid not in update["occupants"]]:
125+
old_occupant.roomNumber = None
126+
log.info('api', action='%s removed from room' % old_occupant.uid)
127+
128+
return jsonify({"success": True}), 200
129+
130+
131+
@housing_bp.route('/housing/room/<rmnumber>', methods=['GET'])
132+
def get_occupants(rmnumber):
133+
134+
# Get the current list of people living on-floor.
135+
current_students = ldap_get_current_students()
136+
137+
# Find the current occupants of the specified room.
138+
occupants = [account.uid for account in current_students
139+
if ldap_get_roomnumber(account) == str(rmnumber)]
140+
return jsonify({"room": rmnumber, "occupants": occupants}), 200

conditional/blueprints/member_management.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,3 +536,24 @@ def introductory_project_submit():
536536
db.session.commit()
537537

538538
return jsonify({"success": True}), 200
539+
540+
@member_management_bp.route('/member/<uid>', methods=['GET'])
541+
def get_member(uid):
542+
log = logger.new(user_name=request.headers.get("x-webauth-user"),
543+
request_id=str(uuid.uuid4()))
544+
log.info('api', action='submit introductory project results')
545+
546+
username = request.headers.get('x-webauth-user')
547+
account = ldap_get_member(username)
548+
549+
if not ldap_is_eval_director(account):
550+
return "must be eval director", 403
551+
552+
member = ldap_get_member(uid)
553+
account_dict = {
554+
"uid": member.uid,
555+
"name": member.cn,
556+
"display": member.displayName
557+
}
558+
559+
return jsonify(account_dict), 200

conditional/templates/housing.html

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,80 @@ <h3 class="panel-title">Housing Queue
4747
<div class="col-xs-12 col-sm-6 col-md-6">
4848
<div class="panel panel-default">
4949
<div class="panel-heading">
50-
<h3 class="panel-title">Current Room Numbers</h3>
50+
<h3 class="panel-title">Current Room Numbers
51+
{% if is_eval_director %}
52+
<button type="button" class="btn btn-primary btn-sm btn-conditional pull-right" data-module="housingEdit" data-modal="editHousing" data-rmnumber="">
53+
<span class="glyphicon glyphicon-plus"></span> Add Room
54+
</button>
55+
{% endif %}
56+
</h3>
5157
</div>
5258
<div class="panel-body table-fill">
5359
<div class="table-responsive">
54-
<table class="table table-striped no-bottom-margin">
60+
<table class="table table-striped no-bottom-margin" id="housing-table">
5561
<tr>
5662
<th>Room</th>
5763
<th>Occupants</th>
64+
{% if is_eval_director %}
65+
<th>Edit</th>
66+
{% endif %}
5867
</tr>
5968
{% for room in room_list %}
6069
<tr>
6170
{% if room != "N/A" %}
6271
<td>
6372
<h3 class="room-number">{{room}}</h3></td>
6473
<td>
65-
{% for m in housing[room] %}
66-
<p class="room-name">{{m}}</p>
67-
{% endfor %}
74+
<ul id="{{room}}" class="occupant-list">
75+
{% for m in housing[room] %}
76+
<li class="room-name">{{m}}</li>
77+
{% endfor %}
78+
</ul>
6879
</td>
69-
{% endif %} {% endfor %}
80+
{% if is_eval_director %}
81+
<td>
82+
<button type="button" class="btn btn-default navbar-btn" data-module="housingEdit" data-modal="editHousing" data-rmnumber="{{room}}" id="rm-edit-btn">
83+
<span class="glyphicon glyphicon-edit attend-edit-icon"></span> Edit
84+
</button>
85+
</td>
86+
{% endif %}
87+
{% endif %}
88+
89+
</tr>
90+
{% endfor %}
7091
</table>
7192
</div>
7293
</div>
7394
</div>
7495
</div>
7596
</div>
7697
</div>
98+
99+
<div class="modal fade" id="editHousing" tabindex="-1">
100+
<div class="vertical-alignment-helper">
101+
<div class="modal-dialog vertical-align-center">
102+
<div class="modal-content">
103+
<div class="modal-header">
104+
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
105+
<h4 class="modal-title" id="editHousingTitle">Edit Room</h4>
106+
</div>
107+
<form method="post">
108+
<div class="modal-body">
109+
<div class="row user-edit-row">
110+
<label class="control-label" for="rmnumber">Room Number</label>
111+
<input type="text" name="rmnumber" class="form-control" />
112+
</div>
113+
<div class="row user-edit-row">
114+
<label class="control-label" for="occupants">Occupants</label>
115+
<input type="text" name="occupants" class="form-control" />
116+
</div>
117+
</div>
118+
<div class="modal-footer">
119+
<input type="submit" class="btn btn-primary" value="Submit">
120+
</div>
121+
</form>
122+
</div>
123+
</div>
124+
</div>
125+
</div>
77126
{% endblock %}

conditional/util/housing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from conditional.models.models import InHousingQueue
77
from conditional.models.models import OnFloorStatusAssigned
88

9+
910
def get_housing_queue(is_eval_director=False):
1011

1112
# Generate a dictionary of dictionaries where the UID is the key
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Enum} from 'enumify';
2+
3+
class HousingException extends Enum {
4+
}
5+
6+
HousingException.initEnum({
7+
SUBMIT_BEFORE_RENDER: {
8+
get message() {
9+
return "Cannot submit updated roster before the modal renders.";
10+
}
11+
}
12+
});
13+
14+
export default HousingException;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/* global fetch */
2+
import 'whatwg-fetch';
3+
import Exception from "../exceptions/exception";
4+
import HousingException from "../exceptions/housingException";
5+
import FetchUtil from "../utils/fetchUtil";
6+
import MemberSelect from "./memberSelect";
7+
8+
export default class EditHousing {
9+
constructor(link) {
10+
this.link = link;
11+
this.modal = null;
12+
this.modalTpl = document.querySelector('#' + this.link.dataset.modal);
13+
this.type = this.modalTpl.dataset.type;
14+
this.rmnumber = this.link.dataset.rmnumber;
15+
16+
this.endpoints = {
17+
roomDetails: '/housing/room/',
18+
alterRoom: '/housing/update/',
19+
memberDetails: '/member/'
20+
};
21+
22+
this.render();
23+
}
24+
25+
render() {
26+
this.link.addEventListener('click', e => {
27+
e.preventDefault();
28+
29+
if (this.rmnumber === "") {
30+
this.data = {};
31+
this.data.occupants = [];
32+
this._renderModal();
33+
} else {
34+
fetch(this.endpoints.roomDetails + this.rmnumber, {
35+
method: 'GET',
36+
headers: {
37+
Accept: 'application/json'
38+
},
39+
credentials: 'same-origin'
40+
})
41+
.then(FetchUtil.checkStatus)
42+
.then(FetchUtil.parseJSON)
43+
.then(data => {
44+
this.data = data;
45+
this._renderModal();
46+
});
47+
}
48+
});
49+
}
50+
51+
_renderModal() {
52+
// Clone template modal
53+
this.modal = this.modalTpl.cloneNode(true);
54+
this.modal.setAttribute('id',
55+
this.modal.getAttribute('id') + '-' + this.rmnumber);
56+
57+
// Submit button
58+
this.modal.querySelector('input[type="submit"]').addEventListener('click',
59+
e => {
60+
e.preventDefault();
61+
this._submitForm();
62+
});
63+
64+
// Room Number
65+
const roomInput = this.modal.querySelector('input[name="rmnumber"]');
66+
roomInput.value = this.rmnumber;
67+
68+
// Occupants
69+
const occupantsInput = this.modal.querySelector('input[name="occupants"]');
70+
let occupantsStr = "";
71+
this.data.occupants.forEach(occupant => {
72+
occupantsStr += occupant + ",";
73+
});
74+
occupantsInput.value = occupantsStr;
75+
76+
// Initialize selector control
77+
occupantsInput.dataset.src = "cm_members";
78+
new MemberSelect(occupantsInput); // eslint-disable-line no-new
79+
80+
// Add to DOM and show, then remove on hide
81+
document.getElementsByTagName('body')[0].appendChild(this.modal);
82+
$(this.modal)
83+
.on('hidden.bs.modal', e => {
84+
document.getElementsByTagName('body')[0].removeChild(e.target);
85+
})
86+
.modal('show');
87+
}
88+
89+
_submitForm() {
90+
if (this.modal) {
91+
this.modal.querySelectorAll('button').forEach(btn => {
92+
btn.disabled = true;
93+
});
94+
95+
// Save details
96+
let payload = {};
97+
payload.occupants = this.modal.querySelector('input[name="occupants"]').value.split(','); // eslint-disable-line max-len
98+
let room = this.modal.querySelector('input[name="rmnumber"]').value;
99+
100+
FetchUtil.post(this.endpoints.alterRoom + room, payload, {
101+
successText: 'Occupants have been updated.'
102+
}, () => {
103+
// Hide the modal.
104+
$(this.modal).modal('hide');
105+
106+
// Update the DOM to reflect the new occupants.
107+
var occupantList = document.getElementById(room);
108+
if (occupantList) {
109+
// The room already exists in the list, update it.
110+
occupantList.innerHTML = '';
111+
payload.occupants.forEach(occupant => {
112+
fetch(this.endpoints.memberDetails + occupant, {
113+
method: 'GET',
114+
headers: {
115+
Accept: 'application/json'
116+
},
117+
credentials: 'same-origin'
118+
})
119+
.then(FetchUtil.checkStatus)
120+
.then(FetchUtil.parseJSON)
121+
.then(data => {
122+
var newName = document.createElement("li");
123+
newName.appendChild(document.createTextNode(data.name));
124+
newName.setAttribute("class", "room-name");
125+
occupantList.appendChild(newName);
126+
});
127+
});
128+
} else {
129+
// The room is new and needs to be created.
130+
var roomTable = document.getElementById("housing-table");
131+
var newRoom = document.createElement("tr");
132+
var newRoomNbrCol = document.createElement("td");
133+
var newRoomNbr = document.createElement("h3");
134+
newRoomNbr.appendChild(document.createTextNode(room));
135+
newRoomNbr.setAttribute("class", "room-number");
136+
newRoomNbrCol.appendChild(newRoomNbr);
137+
newRoomNbrCol.setAttribute("class", "new-table-col");
138+
newRoom.appendChild(newRoomNbrCol);
139+
// Add new occupants to room.
140+
var newOccupantCol = document.createElement("td");
141+
var newOccupantList = document.createElement("ul");
142+
newOccupantList.setAttribute("id", room);
143+
newOccupantList.setAttribute("class", "occupant-list");
144+
payload.occupants.forEach(occupant => {
145+
fetch(this.endpoints.memberDetails + occupant, {
146+
method: 'GET',
147+
headers: {
148+
Accept: 'application/json'
149+
},
150+
credentials: 'same-origin'
151+
})
152+
.then(FetchUtil.checkStatus)
153+
.then(FetchUtil.parseJSON)
154+
.then(data => {
155+
var newName = document.createElement("li");
156+
newName.appendChild(document.createTextNode(data.name));
157+
newName.setAttribute("class", "room-name");
158+
newOccupantList.appendChild(newName);
159+
});
160+
});
161+
newOccupantCol.appendChild(newOccupantList);
162+
newOccupantCol.setAttribute("class", "new-table-col");
163+
newRoom.appendChild(newOccupantCol);
164+
// Add edit button for new room.
165+
var newEditCol = document.createElement("td");
166+
var editBtn = document.getElementById("rm-edit-btn");
167+
var newEditBtn = editBtn.cloneNode(true);
168+
newEditBtn.setAttribute("data-rmnumber", room);
169+
new EditHousing(newEditBtn); // eslint-disable-no-new
170+
newEditCol.appendChild(newEditBtn);
171+
newEditCol.setAttribute("class", "new-table-col");
172+
newRoom.appendChild(newEditCol);
173+
roomTable.appendChild(newRoom);
174+
}
175+
});
176+
} else {
177+
throw new Exception(HousingException.SUBMIT_BEFORE_RENDER);
178+
}
179+
}
180+
}

frontend/stylesheets/pages/_housing.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,12 @@
1111
width: 60px;
1212
text-align: center;
1313
}
14+
15+
.occupant-list {
16+
padding: 0;
17+
list-style: none;
18+
}
19+
20+
.new-table-col {
21+
padding: 8px;
22+
}

0 commit comments

Comments
 (0)