-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Privacy features && social handles #242
Changes from all commits
5a49aaf
e098874
c790285
cc68eec
2922f5a
012e97e
e0f591a
2b5a6a0
c0972d6
dfad172
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
from app import app, db | ||
from app.search import SearchableMixin | ||
from app.util import get_now | ||
from app.util import get_now, PERSISTENT_FIELDS, scrub_hidden_data | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm I don't know how I feel about putting PERSISTENT_FIELDS under util... seems pretty specific to one file. Maybe make this a class variable under PersonPersistent? |
||
import jwt | ||
import datetime | ||
from copy import copy | ||
from sqlalchemy.sql import collate | ||
from sqlalchemy.orm.session import make_transient | ||
|
||
|
||
leaderships = db.Table( | ||
|
@@ -199,7 +201,49 @@ def search(criteria): | |
people = person_query.all() | ||
return people | ||
|
||
def get_persistent_data(self): | ||
return PersonPersistent.query.filter_by(person_id=self.id).first() | ||
|
||
@staticmethod | ||
def search_respect_privacy_include_persistent(criteria): | ||
''' | ||
Performs a search, including persistent data. | ||
- Persistent data, including privacy and social information, is returned. | ||
- If a person's privacy settings hide a certain field, it will be omitted from the results. | ||
- The returned objects will be expunged, and changes to them will not be committed. | ||
''' | ||
people = Person.search(criteria) | ||
people_copy = [] | ||
for person in people: | ||
person_copy = copy(person) | ||
make_transient(person_copy) | ||
|
||
persistent_data = person.get_persistent_data() | ||
if persistent_data is not None: | ||
for field in PERSISTENT_FIELDS: | ||
setattr(person_copy, field, getattr(persistent_data, field)) | ||
|
||
scrub_hidden_data(person_copy) | ||
people_copy.append(person_copy) | ||
return people_copy | ||
|
||
class PersonPersistent(db.Model): | ||
__tablename__ = 'person_persistent' | ||
id = db.Column(db.Integer, primary_key=True, autoincrement=True) | ||
person_id = db.Column(db.Integer, db.ForeignKey('person.id')) | ||
|
||
# socials | ||
socials_instagram = db.Column(db.String) | ||
socials_snapchat = db.Column(db.String) | ||
|
||
# privacy | ||
privacy_hide_image = db.Column(db.Boolean) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fundamentally disagree that we should keep track of what things people have marked as removed. It adds a ton of logic to our operations and risks giving people a totally false sense of security, since many people will stop with Yalies and not actually prevent their data from being accessed in any meaningful way. Instead, I think we should enable users to do a one-time removal of the field in question from our site when they do a removal (i.e. update the person table and remove that field), and aggressively warn that this serves only to accelerate the process of removal after removing one's data from the Face Book/directory. We really, really should not be putting ourselves in a position of needing to remove fields at request-time. I doubt there is any way to make that efficient. |
||
privacy_hide_email = db.Column(db.Boolean) | ||
privacy_hide_room = db.Column(db.Boolean) | ||
privacy_hide_phone = db.Column(db.Boolean) | ||
privacy_hide_address = db.Column(db.Boolean) | ||
privacy_hide_major = db.Column(db.Boolean) | ||
privacy_hide_birthday = db.Column(db.Boolean) | ||
class Group(db.Model): | ||
__tablename__ = 'group' | ||
__searchable__ = ( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
from flask import render_template, make_response, request, redirect, url_for, jsonify, abort, g, session | ||
from app import app, db, scraper, cas | ||
from app.models import User, Person, Key | ||
from app.util import requires_login, to_json, get_now, succ, fail | ||
from app.models import User, Person, Key, PersonPersistent | ||
from app.util import requires_login, forbidden_via_api, to_json, get_now, succ, fail, PERSISTENT_FIELDS, is_valid_instagram_username, is_valid_snapchat_username | ||
from .cas_validate import validate | ||
from sqlalchemy import distinct | ||
|
||
|
@@ -25,12 +25,12 @@ def store_user(): | |
token_cookie = request.cookies.get('token') | ||
if token_cookie: | ||
g.user = User.from_token(token_cookie) | ||
method_used = 'cookie' | ||
g.method_used = 'cookie' | ||
authorization = request.headers.get('Authorization') | ||
if g.user is None and authorization: | ||
token = authorization.split(' ')[-1] | ||
g.user = User.from_token(token) | ||
method_used = 'header' | ||
g.method_used = 'header' | ||
if g.user is None and cas.username: | ||
g.user = User.query.get(cas.username) | ||
if not g.user: | ||
|
@@ -41,14 +41,14 @@ def store_user(): | |
registered_on=timestamp, | ||
admin=is_first_user) | ||
db.session.add(g.user) | ||
method_used = 'CAS' | ||
g.method_used = 'CAS' | ||
if g.user: | ||
g.person = Person.query.filter_by(netid=g.user.id, school_code='YC').first() | ||
if g.user.banned or (not g.person and not g.user.admin): | ||
# TODO: give a more graceful error than just a 403 | ||
abort(403) | ||
try: | ||
print(f'Authorized request by {g.person.first_name} {g.person.last_name} with {method_used} authentication.') | ||
print(f'Authorized request by {g.person.first_name} {g.person.last_name} with {g.method_used} authentication.') | ||
except AttributeError: | ||
print('Could not render name.') | ||
g.user.last_seen = timestamp | ||
|
@@ -237,6 +237,7 @@ def hide_me(): | |
|
||
@app.route('/keys', methods=['GET']) | ||
@requires_login | ||
@forbidden_via_api | ||
def get_keys(): | ||
keys = Key.query.filter_by(user_id=g.user.id, | ||
deleted=False).all() | ||
|
@@ -245,24 +246,17 @@ def get_keys(): | |
|
||
@app.route('/keys', methods=['POST']) | ||
@requires_login | ||
@forbidden_via_api | ||
def create_key(): | ||
payload = request.get_json() | ||
key = g.user.create_key(payload['description']) | ||
db.session.add(key) | ||
db.session.commit() | ||
return to_json(key) | ||
|
||
|
||
""" | ||
@app.route('/keys/<key_id>', methods=['POST']) | ||
@requires_login | ||
def update_key(key_id): | ||
pass | ||
""" | ||
|
||
|
||
@app.route('/keys/<key_id>', methods=['DELETE']) | ||
@requires_login | ||
@forbidden_via_api | ||
def delete_key(key_id): | ||
key = Key.query.get(key_id) | ||
if key.user_id != g.user.id: | ||
|
@@ -271,6 +265,53 @@ def delete_key(key_id): | |
db.session.commit() | ||
return succ('Key deleted.') | ||
|
||
@app.route('/edit', methods=['GET']) | ||
@requires_login | ||
def edit(): | ||
if g.person is None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we check this already |
||
return fail('Could not find person in the database.', 403) | ||
person_persistent = g.person.get_persistent_data() | ||
|
||
if person_persistent is None: | ||
return render_template('edit.html') | ||
return render_template( | ||
'edit.html', | ||
socials_instagram=person_persistent.socials_instagram, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that you can do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although it won't really matter if you remove the |
||
socials_snapchat=person_persistent.socials_snapchat, | ||
privacy_hide_image=person_persistent.privacy_hide_image, | ||
privacy_hide_email=person_persistent.privacy_hide_email, | ||
privacy_hide_room=person_persistent.privacy_hide_room, | ||
privacy_hide_phone=person_persistent.privacy_hide_phone, | ||
privacy_hide_address=person_persistent.privacy_hide_address, | ||
privacy_hide_major=person_persistent.privacy_hide_major, | ||
privacy_hide_birthday=person_persistent.privacy_hide_birthday | ||
) | ||
|
||
@app.route('/edit', methods=['POST']) | ||
@requires_login | ||
@forbidden_via_api | ||
def edit_post(): | ||
payload = request.get_json() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already stored at g.json I think |
||
socials_instagram = payload["socials_instagram"] if hasattr(payload, "socials_instagram") else None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: use single quotes in this function for consistency |
||
socials_snapchat = payload["socials_snapchat"] if hasattr(payload, "socials_snapchat") else None | ||
if (socials_instagram is not None) and (len(socials_instagram) != 0) and (not is_valid_instagram_username(payload["socials_instagram"])): | ||
return fail('Invalid Instagram username.', 400) | ||
if (socials_snapchat is not None) and (len(socials_snapchat) != 0) and (not is_valid_snapchat_username(payload["socials_snapchat"])): | ||
return fail('Invalid Snapchat username.', 400) | ||
|
||
if g.person is None: | ||
return fail('Could not find person in the database.', 403) | ||
person_persistent = g.person.get_persistent_data() | ||
if person_persistent is None: | ||
person_persistent = PersonPersistent(person_id=g.person.id) | ||
db.session.add(person_persistent) | ||
|
||
for key in PERSISTENT_FIELDS: | ||
if key in payload: | ||
setattr(person_persistent, key, payload[key]) | ||
|
||
db.session.commit() | ||
return succ('Person edited.') | ||
|
||
@app.route('/authorize', methods=['POST']) | ||
def api_authorize_cas(): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
from app import app, db, celery, elasticsearch | ||
from app.models import leaderships, Group, Person | ||
from app.models import leaderships, Group, Person, PersonPersistent | ||
from app.mail import send_scraper_report | ||
|
||
from app.scraper import sources | ||
|
@@ -106,8 +106,11 @@ def scrape(caches_active, face_book_cookie, people_search_session_cookie, csrf_t | |
Group.query.delete() | ||
|
||
Person.query.delete() | ||
elasticsearch.indices.delete(index=Person.__tablename__) | ||
elasticsearch.indices.create(index=Person.__tablename__) | ||
|
||
if elasticsearch is not None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great stuff thanks for fixing this |
||
elasticsearch.indices.delete(index=Person.__tablename__) | ||
elasticsearch.indices.create(index=Person.__tablename__) | ||
|
||
num_inserted = 0 | ||
for person_dict in people: | ||
if not person_dict.get('netid'): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
.form-group { | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: flex-start; | ||
align-items: center; | ||
gap: 20px; | ||
margin-bottom: 10px; | ||
} | ||
|
||
h3 { | ||
margin-bottom: 20px; | ||
font-size: 20px; | ||
} | ||
|
||
#error { | ||
color: #FF0000; | ||
} | ||
|
||
#success { | ||
color: #00FF00; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
const FIELDS = [ | ||
'socials_instagram', | ||
// 'socials_snapchat', | ||
'privacy_hide_image', | ||
'privacy_hide_email', | ||
'privacy_hide_room', | ||
'privacy_hide_phone', | ||
'privacy_hide_address', | ||
'privacy_hide_major', | ||
'privacy_hide_birthday' | ||
]; | ||
|
||
const submit = document.getElementById('submit'); | ||
const error = document.getElementById('error'); | ||
const success = document.getElementById('success'); | ||
|
||
submit.onclick = async (e) => { | ||
error.innerText = ''; | ||
success.innerText = ''; | ||
|
||
const data = {}; | ||
for(const field of FIELDS) { | ||
const elem = document.getElementById(field); | ||
if(elem.type === 'checkbox') data[field] = elem.checked; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: put space in between if, for, etc. and the paren to match rest of the code |
||
else if(elem.type === 'text') data[field] = elem.value; | ||
} | ||
let response; | ||
try { | ||
response = await fetch('/edit', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify(data) | ||
}) | ||
} catch(e) { | ||
console.error(e); | ||
error.innerText = 'An unexpected error has occurred.'; | ||
} | ||
if(response.status === 200) { | ||
success.innerText = 'Your profile has been updated'; | ||
} else { | ||
console.error(await response.text()); | ||
const json = await response.json(); | ||
error.innerText = json.message; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function name is way too long