diff --git a/conditional/blueprints/dashboard.py b/conditional/blueprints/dashboard.py index c598101f..9ca7b8ad 100644 --- a/conditional/blueprints/dashboard.py +++ b/conditional/blueprints/dashboard.py @@ -5,6 +5,7 @@ from conditional.util.ldap import ldap_is_onfloor from conditional.util.ldap import ldap_is_active from conditional.util.ldap import ldap_is_intromember +from conditional.util.ldap import ldap_is_current_student from conditional.util.ldap import ldap_get_member from conditional.util.ldap import ldap_get_active_members @@ -44,6 +45,8 @@ def display_dashboard(): data['active'] = ldap_is_active(member) data['onfloor'] = ldap_is_onfloor(member) data['voting'] = bool(member.uid in can_vote) + data['student'] = ldap_is_current_student(member) + data['voting_count'] = {"Voting Members": len(can_vote), "Active Members": len(ldap_get_active_members())} diff --git a/conditional/blueprints/member_management.py b/conditional/blueprints/member_management.py index 98972179..e0da46ca 100644 --- a/conditional/blueprints/member_management.py +++ b/conditional/blueprints/member_management.py @@ -1,11 +1,14 @@ import csv import io +import re from datetime import datetime import structlog -from flask import Blueprint, request, jsonify, abort +from flask import Blueprint, request, jsonify, abort, make_response + +from conditional import app from conditional.models.models import FreshmanAccount from conditional.models.models import FreshmanEvalData @@ -28,6 +31,7 @@ from conditional.util.ldap import ldap_is_financial_director from conditional.util.ldap import ldap_is_active from conditional.util.ldap import ldap_is_onfloor +from conditional.util.ldap import ldap_is_current_student from conditional.util.ldap import ldap_set_roomnumber from conditional.util.ldap import ldap_set_active from conditional.util.ldap import ldap_set_inactive @@ -87,9 +91,11 @@ def display_member_management(): if settings: lockdown = settings.site_lockdown intro_form = settings.intro_form_active + accept_dues_until = settings.accept_dues_until else: lockdown = False intro_form = False + accept_dues_until = datetime.now() return render_template(request, "member_management.html", username=username, @@ -101,6 +107,7 @@ def display_member_management(): freshmen=freshmen_list, co_op=co_op_list, site_lockdown=lockdown, + accept_dues_until=accept_dues_until, intro_form=intro_form) @@ -135,6 +142,31 @@ def member_management_eval(): return jsonify({"success": True}), 200 +@member_management_bp.route('/manage/accept_dues_until', methods=['PUT']) +def member_management_financial(): + log = logger.new(request=request) + + username = request.headers.get('x-webauth-user') + account = ldap_get_member(username) + + if not ldap_is_financial_director(account): + return "must be financial director", 403 + + post_data = request.get_json() + + if 'acceptDuesUntil' in post_data: + date = datetime.strptime(post_data['acceptDuesUntil'], "%Y-%m-%d") + log.info('Changed Dues Accepted Until: {}'.format(date)) + EvalSettings.query.update( + { + 'accept_dues_until': date + }) + + db.session.flush() + db.session.commit() + return jsonify({"success": True}), 200 + + @member_management_bp.route('/manage/user', methods=['POST']) def member_management_adduser(): log = logger.new(request=request) @@ -491,6 +523,23 @@ def member_management_upgrade_user(): return jsonify({"success": True}), 200 +@member_management_bp.route('/manage/make_user_active', methods=['POST']) +def member_management_make_user_active(): + log = logger.new(request=request) + + uid = request.headers.get('x-webauth-user') + account = ldap_get_member(uid) + + if not ldap_is_current_student(account) or ldap_is_active(account): + return "must be current student and not active", 403 + + ldap_set_active(account) + log.info("Make user {} active".format(uid)) + + clear_members_cache() + return jsonify({"success": True}), 200 + + @member_management_bp.route('/manage/intro_project', methods=['GET']) def introductory_project(): log = logger.new(request=request) @@ -583,6 +632,32 @@ def clear_active_members(): return jsonify({"success": True}), 200 +@member_management_bp.route('/manage/export_active_list', methods=['GET']) +def export_active_list(): + sio = io.StringIO() + csvw = csv.writer(sio) + + active_list = [["Full Name", "RIT Username", "Amount to Charge"]] + for member in ldap_get_active_members(): + full_name = member.cn + rit_username = re.search(".*uid=(\\w*)", member.ritDn).group(1) + will_coop = CurrentCoops.query.filter( + CurrentCoops.date_created > start_of_year(), + CurrentCoops.uid == member.uid).first() + dues_per_semester = app.config['DUES_PER_SEMESTER'] + if will_coop: + dues = dues_per_semester + else: + dues = 2 * dues_per_semester + active_list.append([full_name, rit_username, dues]) + + csvw.writerows(active_list) + output = make_response(sio.getvalue()) + output.headers["Content-Disposition"] = "attachment; filename=csh_active_list.csv" + output.headers["Content-type"] = "text/csv" + return output + + @member_management_bp.route('/manage/current/', methods=['POST', 'DELETE']) def remove_current_student(uid): log = logger.new(request=request) diff --git a/conditional/models/models.py b/conditional/models/models.py index aecbdac6..cd4d4563 100644 --- a/conditional/models/models.py +++ b/conditional/models/models.py @@ -243,11 +243,13 @@ class EvalSettings(db.Model): housing_form_active = Column(Boolean) intro_form_active = Column(Boolean) site_lockdown = Column(Boolean) + accept_dues_until = Column(Date) def __init__(self): self.housing_form_active = True self.intro_form_active = True self.site_lockdown = False + self.accept_dues_until = datetime.now() class SpringEval(db.Model): diff --git a/conditional/templates/dashboard.html b/conditional/templates/dashboard.html index e2cc29ab..dee32941 100644 --- a/conditional/templates/dashboard.html +++ b/conditional/templates/dashboard.html @@ -251,7 +251,22 @@

Major Projects

{% endif %} + {% if accepting_dues and student and not active %} +
+
+

Become Active

+
+
+ Hey there, you're eligible to become an active member! Click the button below if you'd like to become active and pay dues. +
+ +
+ {% endif %} + +
{% if housing %}
diff --git a/conditional/templates/member_management.html b/conditional/templates/member_management.html index 6235978f..6a96d5de 100644 --- a/conditional/templates/member_management.html +++ b/conditional/templates/member_management.html @@ -10,7 +10,7 @@

Administration

-
+
{{num_current}}
Current Students
@@ -37,6 +37,11 @@

Administration

+ {% else %} +
+ + +
{% endif %}
@@ -166,7 +171,14 @@

Freshmen Management

-

Member Management

+

+ Member Management + + + +

diff --git a/conditional/util/flask.py b/conditional/util/flask.py index 4d1e356f..19534743 100644 --- a/conditional/util/flask.py +++ b/conditional/util/flask.py @@ -1,3 +1,5 @@ +from datetime import date + from flask import render_template as flask_render_template from conditional.models.models import EvalSettings @@ -27,6 +29,7 @@ def render_template(request, template_name, **kwargs): db.session.commit() account = ldap_get_member(user_name) lockdown = EvalSettings.query.first().site_lockdown + accepting_dues = EvalSettings.query.first().accept_dues_until > date.today() is_active = ldap_is_active(account) is_alumni = ldap_is_alumni(account) is_eboard = ldap_is_eboard(account) @@ -46,6 +49,7 @@ def render_template(request, template_name, **kwargs): return flask_render_template( template_name, lockdown=lockdown, + accepting_dues=accepting_dues, is_active=is_active, is_alumni=is_alumni, is_eboard=is_eboard, diff --git a/config.sample.py b/config.sample.py index 49eaa16a..4fc12ba4 100644 --- a/config.sample.py +++ b/config.sample.py @@ -24,3 +24,6 @@ # Database config SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(os.getcwd(), "data.db")) ZOO_DATABASE_URI = 'mysql+pymysql://user:pass@host/database' + +# General config +DUES_PER_SEMESTER = 80 diff --git a/frontend/javascript/modules/acceptDuesDatepicker.js b/frontend/javascript/modules/acceptDuesDatepicker.js new file mode 100644 index 00000000..b83d9905 --- /dev/null +++ b/frontend/javascript/modules/acceptDuesDatepicker.js @@ -0,0 +1,58 @@ +/* global $ */ +import "bootstrap-material-datetimepicker"; +import "whatwg-fetch"; +import FetchUtil from "../utils/fetchUtil"; +import Exception from "../exceptions/exception"; +import FetchException from "../exceptions/fetchException"; +import sweetAlert from "../../../node_modules/bootstrap-sweetalert/dev/sweetalert.es6.js"; // eslint-disable-line max-len + +export default class DatePicker { + constructor(input) { + this.input = input; + this.endpoint = '/manage/accept_dues_until'; + this.setting = input.dataset.setting; + this.render(); + } + + render() { + $(this.input).bootstrapMaterialDatePicker({ + weekStart: 0, + time: false + }); + + document.getElementsByClassName('dtp-btn-ok')[0].addEventListener('click', + () => { + this._updateSetting(); + }); + } + + _updateSetting() { + console.log("Update dues until: " + this.input.value); + let payload = {}; + payload[this.setting] = this.input.value; + + 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) { + sweetAlert("Uh oh...", "We're having trouble submitting this " + + "form right now. Please try again later.", "error"); + throw new Exception(FetchException.REQUEST_FAILED, response); + } + }) + .catch(error => { + sweetAlert("Uh oh...", "We're having trouble submitting this " + + "form right now. Please try again later.", "error"); + throw new Exception(FetchException.REQUEST_FAILED, error); + }); + } +} diff --git a/frontend/javascript/modules/becomeActive.js b/frontend/javascript/modules/becomeActive.js new file mode 100644 index 00000000..4de6d1f0 --- /dev/null +++ b/frontend/javascript/modules/becomeActive.js @@ -0,0 +1,26 @@ +import FetchUtil from "../utils/fetchUtil"; + +export default class becomeActive { + constructor(link) { + this.link = link; + this.endpoint = '/manage/make_user_active'; + this.render(); + } + + render() { + this.link.addEventListener('click', e => this._delete(e)); + } + + _delete(e) { + e.preventDefault(); + + FetchUtil.postWithWarning(this.endpoint, {}, { + warningText: "Becoming an active member means that you will be charged" + + " dues, which are detailed in the CSH Constitution.", + successText: "You are now an active member." + }, () => { + document.getElementById('becomeActive').remove(); + }); + } +} + diff --git a/frontend/stylesheets/pages/_management.scss b/frontend/stylesheets/pages/_management.scss index 23e70bc1..ba5d2d07 100644 --- a/frontend/stylesheets/pages/_management.scss +++ b/frontend/stylesheets/pages/_management.scss @@ -12,6 +12,17 @@ margin: 30px 0; } +.btn-get-active { + float: right; + margin-top: -4px; + box-shadow: none; + padding: 3px 7px; +} + +.accept-dues-until { + margin-top: 10px; +} + .upload-title { padding-top: 20px; height: 55px; diff --git a/migrations/versions/d1a06ab54211_.py b/migrations/versions/d1a06ab54211_.py new file mode 100644 index 00000000..e0e3c41b --- /dev/null +++ b/migrations/versions/d1a06ab54211_.py @@ -0,0 +1,26 @@ +"""Add Accept Dues Until site setting + +Revision ID: d1a06ab54211 +Revises: 117567def844 +Create Date: 2017-07-21 17:09:37.540766 + +""" + +# revision identifiers, used by Alembic. +revision = 'd1a06ab54211' +down_revision = '117567def844' + +from alembic import op +import sqlalchemy as sa +from datetime import datetime + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('settings', sa.Column('accept_dues_until', sa.Date(), server_default=datetime.now().strftime("%Y-%m-%d"), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('settings', 'accept_dues_until') + # ### end Alembic commands ###