diff --git a/conditional/blueprints/cache_management.py b/conditional/blueprints/cache_management.py index 43b4a7ae..f86b0586 100644 --- a/conditional/blueprints/cache_management.py +++ b/conditional/blueprints/cache_management.py @@ -38,8 +38,9 @@ def restart_app(): @cache_bp.route('/clearcache') def clear_cache(): user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name) or not ldap_is_rtp(user_name): + if not ldap_is_eval_director(account) or not ldap_is_rtp(account): return redirect("/dashboard") logger.info('api', action='purge system cache') diff --git a/conditional/blueprints/conditional.py b/conditional/blueprints/conditional.py index 96cc1ac3..8dce1ad8 100644 --- a/conditional/blueprints/conditional.py +++ b/conditional/blueprints/conditional.py @@ -53,8 +53,9 @@ def create_conditional(): log.info('api', action='create new conditional') user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name): + if not ldap_is_eval_director(account): return "must be eval director", 403 post_data = request.get_json() @@ -78,8 +79,9 @@ def conditional_review(): # get user data user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name): + if not ldap_is_eval_director(account): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -106,7 +108,9 @@ def conditional_delete(cid): log.info('api', action='delete conditional') user_name = request.headers.get('x-webauth-user') - if ldap_is_eval_director(user_name): + account = ldap_get_member(user_name) + + if ldap_is_eval_director(account): Conditional.query.filter( Conditional.id == cid ).delete() diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index 7d9815d3..b4d0a40b 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -17,7 +17,7 @@ from conditional.blueprints.member_management import get_members_info -from conditional.util.housing import get_queue_length, get_queue_position +from conditional.util.housing import get_queue_position from conditional.util.flask import render_template from conditional.util.member import get_freshman_data, get_voting_members @@ -73,8 +73,8 @@ def display_dashboard(): housing = dict() housing['points'] = member.housingPoints housing['room'] = member.roomNumber - if housing['room'] == "": - housing['queue_pos'] = "%s / %s" % (get_queue_position(member.uid), get_queue_length()) + if housing['room'] is None: + housing['queue_pos'] = "%s / %s" % get_queue_position(member.uid) else: housing['queue_pos'] = "N/A" else: diff --git a/conditional/blueprints/housing.py b/conditional/blueprints/housing.py index 6b20a8f3..71624100 100644 --- a/conditional/blueprints/housing.py +++ b/conditional/blueprints/housing.py @@ -1,15 +1,18 @@ import uuid import structlog -from flask import Blueprint, request +from flask import Blueprint, request, jsonify from conditional.models.models import FreshmanAccount -from conditional.util.housing import get_queue_with_points -from conditional.util.ldap import ldap_get_onfloor_members +from conditional.models.models import InHousingQueue +from conditional.util.housing import get_housing_queue +from conditional.util.ldap import ldap_get_onfloor_members, ldap_is_eval_director, ldap_get_member from conditional.util.flask import render_template from conditional.util.ldap import ldap_get_roomnumber +from conditional import db + logger = structlog.get_logger() @@ -23,8 +26,8 @@ def display_housing(): log.info('frontend', action='display housing') # get user data - user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) housing = {} onfloors = [account for account in ldap_get_onfloor_members()] @@ -57,6 +60,33 @@ def display_housing(): return render_template(request, 'housing.html', username=user_name, - queue=get_queue_with_points(), + queue=get_housing_queue(ldap_is_eval_director(account)), housing=housing, room_list=sorted(list(room_list))) + + +@housing_bp.route('/housing/in_queue', methods=['PUT']) +def change_queue_state(): + log = logger.new(user_name=request.headers.get("x-webauth-user"), + request_id=str(uuid.uuid4())) + log.info('api', action='add or remove member from housing queue') + + username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) + + if not ldap_is_eval_director(account): + return "must be eval director", 403 + + post_data = request.get_json() + uid = post_data.get('uid', False) + + if uid: + if post_data.get('inQueue', False): + queue_obj = InHousingQueue(uid=uid) + db.session.add(queue_obj) + else: + InHousingQueue.query.filter_by(uid=uid).delete() + + db.session.flush() + db.session.commit() + return jsonify({"success": True}), 200 diff --git a/conditional/blueprints/major_project_submission.py b/conditional/blueprints/major_project_submission.py index 1eebc2b8..bc593aad 100644 --- a/conditional/blueprints/major_project_submission.py +++ b/conditional/blueprints/major_project_submission.py @@ -78,8 +78,9 @@ def major_project_review(): # get user data user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name): + if not ldap_is_eval_director(account): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -106,12 +107,14 @@ def major_project_delete(pid): # get user data user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) + major_project = MajorProject.query.filter( MajorProject.id == pid ).first() creator = major_project.uid - if creator == user_name or ldap_is_eval_director(user_name): + if creator == user_name or ldap_is_eval_director(account): MajorProject.query.filter( MajorProject.id == pid ).delete() diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 08fd27d4..91387801 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -56,8 +56,9 @@ def display_member_management(): log.info('frontend', action='display member management') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username) and not ldap_is_financial_director(username): + if not ldap_is_eval_director(account) and not ldap_is_financial_director(account): return "must be eval director", 403 member_list = get_members_info() @@ -102,8 +103,9 @@ def member_management_eval(): log.info('api', action='submit site settings') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 post_data = request.get_json() @@ -134,8 +136,9 @@ def member_management_adduser(): log.info('api', action='add fid user') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 post_data = request.get_json() @@ -158,8 +161,9 @@ def member_management_adduser(): @member_management_bp.route('/manage/user/upload', methods=['POST']) def member_management_uploaduser(): username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 f = request.files['file'] @@ -195,8 +199,9 @@ def member_management_edituser(uid): log.info('api', action='edit uid user') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username) and not ldap_is_financial_director(username): + if not ldap_is_eval_director(account) and not ldap_is_financial_director(account): return "must be eval director", 403 post_data = request.get_json() @@ -219,7 +224,8 @@ def edit_uid(uid, username, post_data): onfloor_status = post_data['onfloorStatus'] housing_points = post_data['housingPoints'] - if ldap_is_eval_director(username): + current_account = ldap_get_member(username) + if ldap_is_eval_director(current_account): logger.info('backend', action="edit %s room: %s onfloor: %s housepts %s" % (uid, post_data['roomNumber'], post_data['onfloorStatus'], post_data['housingPoints'])) @@ -296,8 +302,9 @@ def member_management_getuserinfo(uid): log.info('api', action='retrieve user info') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username) and not ldap_is_financial_director(username): + if not ldap_is_eval_director(account) and not ldap_is_financial_director(account): return "must be eval or financial director", 403 acct = None @@ -341,7 +348,7 @@ def get_hm_date(hm_id): account = ldap_get_member(uid) - if ldap_is_eval_director(username): + if ldap_is_eval_director(ldap_get_member(username)): missed_hm = [ { 'date': get_hm_date(hma.meeting_id), @@ -382,8 +389,9 @@ def member_management_deleteuser(fid): log.info('api', action='edit fid user') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 if not fid.isdigit(): @@ -417,8 +425,9 @@ def member_management_upgrade_user(): log.info('api', action='convert fid to uid entry') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 post_data = request.get_json() @@ -479,8 +488,9 @@ def introductory_project(): log.info('api', action='show introductory project management') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 return render_template(request, @@ -496,8 +506,9 @@ def introductory_project_submit(): log.info('api', action='submit introductory project results') username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) - if not ldap_is_eval_director(username): + if not ldap_is_eval_director(account): return "must be eval director", 403 post_data = request.get_json() diff --git a/conditional/blueprints/slideshow.py b/conditional/blueprints/slideshow.py index 7647de08..5ed0055b 100644 --- a/conditional/blueprints/slideshow.py +++ b/conditional/blueprints/slideshow.py @@ -11,7 +11,7 @@ from conditional.blueprints.intro_evals import display_intro_evals from conditional.blueprints.spring_evals import display_spring_evals -from conditional.util.ldap import ldap_is_eval_director +from conditional.util.ldap import ldap_is_eval_director, ldap_get_member from conditional.models.models import FreshmanEvalData from conditional.models.models import SpringEval @@ -31,7 +31,9 @@ def slideshow_intro_display(): log.info('frontend', action='display intro slideshow') user_name = request.headers.get('x-webauth-user') - if not ldap_is_eval_director(user_name): + account = ldap_get_member(user_name) + + if not ldap_is_eval_director(account): return redirect("/dashboard") return render_template(request, @@ -61,8 +63,9 @@ def slideshow_intro_review(): # get user data user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name): + if not ldap_is_eval_director(account): return redirect("/dashboard", code=302) post_data = request.get_json() @@ -90,7 +93,9 @@ def slideshow_spring_display(): log.info('frontend', action='display membership evaluations slideshow') user_name = request.headers.get('x-webauth-user') - if not ldap_is_eval_director(user_name): + account = ldap_get_member(user_name) + + if not ldap_is_eval_director(account): return redirect("/dashboard") return render_template(request, @@ -120,8 +125,9 @@ def slideshow_spring_review(): # get user data user_name = request.headers.get('x-webauth-user') + account = ldap_get_member(user_name) - if not ldap_is_eval_director(user_name): + if not ldap_is_eval_director(account): return redirect("/dashboard", code=302) post_data = request.get_json() diff --git a/conditional/models/docs.md b/conditional/models/docs.md index 13d19b84..33697e10 100644 --- a/conditional/models/docs.md +++ b/conditional/models/docs.md @@ -169,7 +169,12 @@ Records the yearly results of member's spring evaluations. | `date_created` | `TIMESTAMP` | The date of the evaluation. | `status` | `ENUM` | Result of the evaluation. +## InHousingQueue table ## +Records the yearly results of member's spring evaluations. +| Field | Type | Description | +| ------------- | ------------- | ------------------- | +| `uid` | `VARCHAR(32)` | LDAP uid of the member in the housing queue. ### Member state ### diff --git a/conditional/models/models.py b/conditional/models/models.py index 2a65acec..21a16289 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -248,6 +248,10 @@ class SpringEval(db.Model): name="spring_eval_enum"), nullable=False) +class InHousingQueue(db.Model): + __tablename__ = 'in_housing_queue' + uid = Column(String(32), primary_key=True) + def __init__(self, uid): self.uid = uid self.active = True diff --git a/conditional/templates/housing.html b/conditional/templates/housing.html index 65ce6760..97e6bde4 100644 --- a/conditional/templates/housing.html +++ b/conditional/templates/housing.html @@ -6,24 +6,36 @@
-
+
-

Housing Queue

+

Housing Queue + {% if is_eval_director %} + + {% endif %} +

- +
+ {% if is_eval_director %} + + {% endif %} {% for m in queue %} - + + {% if is_eval_director %} + + {% endif %} {% endfor %} diff --git a/conditional/util/housing.py b/conditional/util/housing.py index 19268c3a..dd64f34d 100644 --- a/conditional/util/housing.py +++ b/conditional/util/housing.py @@ -1,16 +1,48 @@ -def get_housing_queue(): - return [] +from datetime import datetime +from conditional.util.ldap import ldap_get_current_students +from conditional.util.ldap import ldap_is_onfloor -def get_queue_with_points(): - return [] +from conditional.models.models import InHousingQueue +from conditional.models.models import OnFloorStatusAssigned +def get_housing_queue(is_eval_director=False): -def get_queue_length(): - # TODO: Actually implement queue length. - return 20 + # Generate a dictionary of dictionaries where the UID is the key + # and {'time': } is the value. We are doing a left + # outer join on the two tables to get a single result that has + # both the member's UID and their on-floor datetime. + in_queue = {entry.uid: {'time': entry.onfloor_granted} for entry + in InHousingQueue.query.outerjoin(OnFloorStatusAssigned, + OnFloorStatusAssigned.uid == InHousingQueue.uid)\ + .with_entities(InHousingQueue.uid, OnFloorStatusAssigned.onfloor_granted)\ + .all()} + + # Populate a list of dictionaries containing the name, username, + # and on-floor datetime for each member who has on-floor status, + # is not already assigned to a room and is in the above query. + queue = [{"uid": account.uid, + "name": account.cn, + "points": account.housingPoints, + "time": in_queue.get(account.uid, {}).get('time', datetime.now()) or datetime.now(), + "in_queue": account.uid in in_queue} + for account in ldap_get_current_students() + if ldap_is_onfloor(account) and (is_eval_director or account.uid in in_queue) + and account.roomNumber is None] + + # Sort based on time (ascending) and then points (decending). + queue.sort(key=lambda m: m['time']) + queue.sort(key=lambda m: m['points'], reverse=True) + + return queue def get_queue_position(username): - # TODO: Actually implement queue position. - return len(username) + + queue = get_housing_queue() + try: + index = next(index for (index, d) in enumerate(get_housing_queue()) + if d["uid"] == username) + 1 + except KeyError: + index = 0 + return (index, len(queue)) diff --git a/conditional/util/ldap.py b/conditional/util/ldap.py index 8fdad08a..23ea51d4 100644 --- a/conditional/util/ldap.py +++ b/conditional/util/ldap.py @@ -27,8 +27,8 @@ def _ldap_remove_member_from_group(account, group): @lru_cache(maxsize=1024) def _ldap_is_member_of_directorship(account, directorship): directors = ldap.get_directorship_heads(directorship) - for account in directors: - if account.uid == account.uid: + for director in directors: + if director.uid == account.uid: return True diff --git a/frontend/javascript/modules/housingQueue.js b/frontend/javascript/modules/housingQueue.js new file mode 100644 index 00000000..f63d7cc6 --- /dev/null +++ b/frontend/javascript/modules/housingQueue.js @@ -0,0 +1,119 @@ +/* global fetch */ +import 'whatwg-fetch'; +import Exception from '../exceptions/exception'; +import FetchException from '../exceptions/fetchException'; +import FetchUtil from '../utils/fetchUtil'; +import sweetAlert from '../../../node_modules/bootstrap-sweetalert/dev/sweetalert.es6.js'; // eslint-disable-line max-len + +export default class HousingQueue { + constructor(queuePanel) { + this.queuePanel = queuePanel; + this.queueTable = this.queuePanel.querySelector('table'); + this.endpoint = '/housing/in_queue'; + this.filteredState = false; + + // Is the housing queue already a DataTable? + if ($.fn.dataTable && $.fn.dataTable.isDataTable(this.queueTable)) { + // Yes, render + this.render(); + } else { + // No, wait until it initializes before rendering + // Must use jQuery to listen to events fired by DataTables + $(this.queueTable).on('init.dt', () => this.render()); + } + } + + render() { + // Remove the event listner so this module doesn't try to render again + $(this.queueTable).off('init.dt'); + + // Retrieve the queue table's DataTables API object + this.queueTableApi = $(this.queueTable).DataTable({ // eslint-disable-line new-cap + retrieve: true + }); + + // Add custom filtering function + $.fn.dataTable.ext.afnFiltering.push(HousingQueue._inQueueFilter); + + this.bindFilterButton(); + this.bindCheckboxes(); + } + + bindFilterButton() { + const filterButton = this.queuePanel.querySelector('#queueFilterToggle'); + filterButton.addEventListener('click', () => { + if (this.queueTable.dataset.show === 'all') { + filterButton.innerHTML = + filterButton.innerHTML.replace('Show Current Queue', 'Show All'); + this.queueTable.dataset.show = 'current'; + } else { + filterButton.innerHTML = + filterButton.innerHTML.replace('Show All', 'Show Current Queue'); + this.queueTable.dataset.show = 'all'; + } + + this.queueTableApi.draw(); + }); + } + + bindCheckboxes() { + this.queuePanel.querySelectorAll('.col-in-queue > input[type="checkbox"]') + .forEach(toggle => { + toggle.addEventListener('click', () => { + const row = toggle.parentNode.parentNode; + this.updateInQueue(toggle.dataset.uid, toggle.checked, row); + }); + }); + } + + updateInQueue(uid, inQueue, row) { + let payload = { + uid: uid, + inQueue: inQueue + }; + + fetch(this.endpoint, { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify(payload) + }) + .then(FetchUtil.checkStatus) + .then(FetchUtil.parseJSON) + .then(response => { + if (response.hasOwnProperty('success') && + response.success === true) { + if (inQueue) { + row.classList.remove('disabled'); + } else { + row.classList.add('disabled'); + } + } else { + sweetAlert('Uh oh...', 'We\'re having trouble updating ' + + 'the queue right now. Please try again later.', 'error'); + throw new Exception(FetchException.REQUEST_FAILED, response); + } + }) + .catch(error => { + sweetAlert('Uh oh...', 'We\'re having trouble updating ' + + 'the queue right now. Please try again later.', 'error'); + throw new Exception(FetchException.REQUEST_FAILED, error); + }); + } + + /* + * Custom filtering function that will remove rows that are not in the housing queue (unselected) + */ + static _inQueueFilter(settings, data, dataIndex) { + // Check to see if we should apply the filter + if (settings.nTable.dataset.show === 'current') { + return settings.aoData[dataIndex].anCells[2] + .querySelector('input[type=checkbox]').checked; + } + + return true; + } +} diff --git a/frontend/stylesheets/pages/_housing.scss b/frontend/stylesheets/pages/_housing.scss index d94230a8..3e382416 100644 --- a/frontend/stylesheets/pages/_housing.scss +++ b/frontend/stylesheets/pages/_housing.scss @@ -6,3 +6,8 @@ margin-top: 17px; padding-left: 20px; } + +.col-in-queue { + width: 60px; + text-align: center; +} diff --git a/frontend/stylesheets/partials/_global.scss b/frontend/stylesheets/partials/_global.scss index 98cd94a9..f3042c41 100644 --- a/frontend/stylesheets/partials/_global.scss +++ b/frontend/stylesheets/partials/_global.scss @@ -58,3 +58,9 @@ .page-title { margin: 20px 0 30px; } + +tr { + &.disabled { + opacity: .5; + } +} diff --git a/migrations/versions/65943c537c84_.py b/migrations/versions/65943c537c84_.py new file mode 100644 index 00000000..9c53fbd9 --- /dev/null +++ b/migrations/versions/65943c537c84_.py @@ -0,0 +1,29 @@ +"""empty message + +Revision ID: 65943c537c84 +Revises: 983d69afb7f8 +Create Date: 2016-12-18 15:16:12.922122 + +""" + +# revision identifiers, used by Alembic. +revision = '65943c537c84' +down_revision = '983d69afb7f8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('in_housing_queue', + sa.Column('uid', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('in_housing_queue') + ### end Alembic commands ###
Member Housing PointsIn Queue?
{{m['name']}} {{m['points']}}