def shared_schedule(school): """Get a user by the secret on one of their schedules and return just that schedule's info.""" school = app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': False}) c = mongo.SchoolCollections(school['fragment']) user = util.User( c.user.find_one_or_404( {'schedules.secret': flask.request.args['secret']}, { 'email': True, # For linking to instructors - do not print 'first': True, 'middle': True, 'last': True, 'schedules.$': True, }, ), school) sch = user['schedules'][0] sch['sections'] = [ sect for sect in sch['sections'] if sect['status'] != 'no' ] addInstructorCourses(c, user, term=sch['term']) schedules = user.formatSchedules() return flask.jsonify(name=user.name(), schedules=schedules)
def reset_password(school): request = flask.request.json school = app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': False}) c = mongo.SchoolCollections(school['fragment']) user = c.user.find_one_or_404({ 'email': request['email'], }, { '_id': True, 'email': True, 'first': True, 'middle': True, 'last': True, }) user = util.User(user, school) user['name'] = user.name() secret = util.create_verification(c, 'reset_password', user=user['_id']) msg = flask_mail.Message( "Password Reset", recipients=user['email'][0:1], ) msg.html = flask.render_template( 'email/reset_password.html', school=school, user=user.user, secret=secret, ) msg.body = flask.render_template( 'email/reset_password.txt', school=school, user=user.user, secret=secret, ) app.mail.send(msg) return flask.jsonify()
def main(args): parser = argparse.ArgumentParser(description='Add a user.') parser.add_argument('--school', required=True) parser.add_argument('--email', required=True, action='append') parser.add_argument('--first') parser.add_argument('--middle') parser.add_argument('--last') parser.add_argument('--password') parser.add_argument('--passhash') parser.add_argument('--role', default=[], action='append') args = parser.parse_args(args) school = app.mongo.db.schools.find_one({'fragment': args.school}) if not school: sys.stderr.write('School %s does not exist.\n' % args.school) sys.exit(1) user = util.User( { 'email': args.email, 'first': args.first, 'middle': args.middle, 'last': args.last, 'schedules': [], 'secret': util.generate_secret(), 'roles': args.role, }, school) if args.password and not args.passhash: user.set_password(args.password) elif args.passhash: user['passhash'] = args.passhash else: sys.stderr.write( 'Exactly one of --passhash, --password must be provided.\n') sys.exit(1) c = mongo.SchoolCollections(school['fragment']) c.user.insert(user.user)
def quicksearch(school, term): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term_obj = c.term.find_one_or_404({'fragment': term}, { '_id': False, 'id': True }) query = util.make_searchable(re.escape(request.args['query'])) courses = c.course.find( { 'term': term_obj['id'], '$or': [ { 'searchable_code': { '$regex': query } }, { 'searchable_name': { '$regex': query } }, ], }, { '_id': False, 'name': True, 'code': True, 'fragment': True }, limit=app.config.get('SCHDL_QUICKSEARCH_LIMIT', 100)) courses = list(courses) courses.sort(key=functools.partial(_quicksearch_sort_key, query)) return flask.Response(json.dumps(courses), mimetype='application/json')
def recommender(school, term): if 'RECOMMENDER_URL' not in app.config: return flask.Response('[]', 'application/json') app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term_obj = c.term.find_one_or_404({'fragment': term}, { '_id': False, 'id': True }) query = urllib.urlencode([ (key, val) for key, val in request.args.iteritems(multi=True) if key in ('course', 'exclude') ]) url = '{}?{}'.format(app.config['RECOMMENDER_URL'], query) LOGGER.debug('Requesting recommendations from %s', url) result = urllib2.urlopen(url) try: continuity_ids = json.load(result)['recommendations'] finally: result.close() LOGGER.debug('Got %s courses', len(continuity_ids)) courses = list( c.course.find( { 'continuity_id': { '$in': continuity_ids }, 'term': term_obj['id'], }, util.project_course_for_search.mongo())) # TODO: sort by recommended order LOGGER.debug('Found %s courses in term %s', len(courses), term) return flask.Response(json.dumps(courses), mimetype='application/json')
def _formatSchedule(self, term, schedule, timestamps=False): out = { 'term': term, # TODO(eitan): workaround for users with schedules with no secret - # fix the data! 'secret': schedule.get('secret', ''), } sections = [] c = mongo.SchoolCollections(self.school['fragment']) for schdl_section in schedule['sections']: course = c.course.find_one({'sections.id': schdl_section['id']}, { '_id': False, 'sections.$': True, 'name': True, 'code': True, 'fragment': True, 'independent_study': True, 'requirements': True, 'continuity_id': True, }) section = formatCourseSection(c, term, course, schdl_section['id']) section['user_status'] = schdl_section['status'] if timestamps: # TODO(eitan): workaround for users with classes in their # schedules that were added before 'updated' field # existed section['user_updated'] = schdl_section.get( 'updated', datetime.datetime(2012, 1, 1)) sections.append(section) out['course_sections'] = sections return out
def _requirementsForSchool(school): requirements = list( mongo.SchoolCollections(school).requirement.find( None, {'_id': False}, )) requirements.sort(key=lambda x: x['short']) return list(requirements)
def main(args): c = mongo.SchoolCollections('brandeis') for i, line in enumerate(sys.stdin, 1): try: data = json.loads(line) user = { 'first': data['first'], 'middle': data['middle'], 'last': data['last'], 'passhash': data['passhash'], 'email': [data['email']] + data['additional_emails'], 'schedules': [], 'roles': [], } # TODO(eitan): set user['_id'] using created date # bson.objectid.ObjectId.from_datetime( # datetime.datetime.strptime( # data['created'], # '%Y-%m-%d' %H:%M:%S')) # then fix for uniqueness user['secret'] = data.get('sharing_key') if not user['secret']: user['secret'] = util.generate_secret() for sch in data['schedules']: term = sch['id'] if term < '1071': LOGGER.warning('Dropping data from term %s for user %s', term, data['email']) continue new_sch = { 'term': term, 'sections': [], 'updated': datetime.datetime.utcnow(), } new_sch['secret'] = (sch['sharing_key'] or util.generate_secret()) for section in sch['sections']: try: sect = { 'status': section['user_status'], 'id': find_section(term, section, c), 'updated': datetime.datetime.utcnow(), } except ValueError as e: logging.error('At line %s:', i) logging.error(e) continue if section['user_color']: sect['color'] = section['user_color'] new_sch['sections'].append(sect) user['schedules'].append(new_sch) if data['is_admin']: user['roles'].append('admin') c.user.insert(user) except: LOGGER.error('Exception while processing line %s', i) raise
def search(school): school = app.mongo.db.schools.find_one_or_404( {'fragment': school}, { 'requirements': True, 'fragment': True, }, ) # Exclude courses without sections query = {'sections.0': {'$exists': 1}} params = request.args if params.get('independent_study', 'false') != 'true': query['independent_study'] = False if params.get('closed', 'false') != 'true': query['sections.status'] = {'$in': ['open', 'restricted']} c = mongo.SchoolCollections(school['fragment']) if 'term' in params: term = c.term.find_one_or_404({'fragment': params['term']}, { 'id': True, '_id': False }) query['term'] = term['id'] if 'req' in params: operator = '$in' if 'reqAny' in params else '$all' query['requirements'] = {operator: params.getlist('req')} if 'subj' in params: query['subjects.id'] = {'$in': params.getlist('subj')} if 'instr' in params: query['sections.instructors'] = {'$in': params.getlist('instr')} if 'q' in params: q = util.make_searchable(re.escape(request.args['q'])) query['$or'] = [ { 'searchable_code': { '$regex': q } }, { 'searchable_name': { '$regex': q } }, { 'searchable_description': { '$regex': q } }, { 'sections.searchable_details': { '$regex': q } }, ] results = list(c.course.find(query, util.project_course_for_search.mongo())) return flask.Response(json.dumps(results), mimetype='application/json')
def term(school, term): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) out = mongo.SchoolCollections(school).term.find_one_or_404( {'fragment': term}, { '_id': False, 'id': False }) out['subjects'] = [subj for subj in out['subjects'] if subj['hasCourses']] out['subjects'].sort(key=lambda x: x['name']) return json.jsonify(out)
def formatSchedules(self, timestamps=False): schedules = [] c = mongo.SchoolCollections(self.school['fragment']) for schedule in self.user['schedules']: term = c.term.find_one( {'id': schedule['term']}, project_term.mongo(), ) schedules.append(self._formatSchedule(term, schedule, timestamps)) return schedules
def _termsForSchool(school): terms = mongo.SchoolCollections(school).term.find( {'hasCourses': True}, { 'subjects': False, '_id': False, 'id': False, 'hasCourses': False }, ) return sorted(terms, key=lambda t: t['start'], reverse=True)
def subject_lookup(school, term): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term = c.term.find_one_or_404({'fragment': term}, { 'subjects.name': True, 'subjects.id': True, '_id': False }) query = request.args.getlist('subj') subjects = [subj for subj in term['subjects'] if subj['id'] in query] return flask.Response(json.dumps(subjects), mimetype='application/json')
def delete_schedule_section(user_id, school_id, term_id, section_id): c = mongo.SchoolCollections(school_id) return c.user.update( { '_id': user_id, 'schedules.term': term_id, }, { '$set': {'schedules.$.updated': datetime.datetime.utcnow()}, '$pull': {'schedules.$.sections': {'id': section_id}}, } )
def import_update(school): if flask.request.headers.get('X-Appengine-Cron') != 'true': return flask.jsonify(reason='permission_denied'), 403 school_fragment = school school = app.mongo.db.schools.find_one_or_404( {'fragment': school_fragment}) c = mongo.SchoolCollections(school_fragment) results = process_update(school, c) if results is not None: LOGGER.info('%s new, %s updates, %s same, %s deletes', *results) return flask.jsonify(status='ok')
def test_load_user(self): with test_data.Database.WithTestData() as data: c = mongo.SchoolCollections(data.school['fragment']) user = c.user.find_one() result = sessions.load_user('bad-id') self.assertIsNone(result) result = sessions.load_user('5269cb4dca50001cdb0a5daa') self.assertIsNone(result) result = sessions.load_user('%s:%s' % (data.school['fragment'], user['_id'])) data.user['_id'] = user['_id'] self.assertEqual(result.user, data.user)
def course(school, term, course): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term = c.term.find_one_or_404({'fragment': term}, {'_id': False}) subjects = term['subjects'] del term['subjects'] course = c.course.find_one_or_404({ 'term': term['id'], 'fragment': course }, {'_id': False}) for section in course['sections']: if c.school_fragment == 'brandeis': section['registration_id'] = section['id'].split('-')[1] section['books_url'] = util.brandeis_books_url( term, course, section) section['instructors'] = util.get_instructors(c, section) util.encode_my_id(section) out = course course_subjects = set(subj['id'] for subj in out['subjects']) subjects = [subj for subj in subjects if subj['id'] in course_subjects] # This currently fails for a bunch of courses with code RBIF xyz because # they point to non-existent subjects. # assert len(subjects) == len(course_subjects) for subj in subjects: del subj['segments'] other_terms = [] for alt_course in c.course.find( { 'continuity_id': course['continuity_id'], 'id': { '$ne': course['id'] } }, { 'term': True, 'name': True, 'fragment': True, '_id': False }): alt_term = c.term.find_one({'id': alt_course['term']}, { '_id': False, 'fragment': True, 'name': True, 'end': True }) assert alt_term alt_course['term'] = alt_term other_terms.append(alt_course) out['term'] = term out['subjects'] = subjects out['other_terms'] = other_terms return json.jsonify(out)
def load_user(user_id): try: school, user_id = user_id.split(':', 1) except ValueError: return None try: user_id = objectid.ObjectId(user_id) except objectid.InvalidId: return None school = app.mongo.db.schools.find_one({'fragment': school}) c = mongo.SchoolCollections(school['fragment']) user = c.user.find_one({'_id': user_id}) if user: return util.User(user, school)
def verify(school): request = flask.request.json school = app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': False}) c = mongo.SchoolCollections(school['fragment']) verification = c.email_verification.find_one_or_404({ 'secret': request['secret'], }) type_ = verification['type'] # Check that it's still valid if verification.get('used'): return flask.jsonify(type=type_, status='used') elif (verification['expiration'].replace(tzinfo=None) < datetime.datetime.utcnow()): return flask.jsonify(type=type_, status='expired') if type_ == 'new_user': try: c.user.insert(verification['user'], w=1) except pymongo.errors.DuplicateKeyError: return flask.jsonify(type=type_, status='account_exists') elif type_ == 'reset_password': if 'password' not in request: return flask.jsonify(type=type_, status='need_password') user = util.User({}, school) user.set_password(request['password']) c.user.update({ '_id': verification['user'], }, { '$set': { 'passhash': user['passhash'] }, }, w=1) elif type_ == 'add_email': c.user.update({ '_id': verification['user'], }, {'$addToSet': { 'email': verification['email'] }}) else: raise ValueError('Unknown verification type %s' % type_) c.email_verification.update({ '_id': verification['_id'], }, { '$set': { 'used': True }, }) return flask.jsonify(type=type_, status='success')
def qs_instructor(school): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) query = util.make_searchable(re.escape(request.args['query'])) instructors = list( c.instructor.find({'searchable_name': { '$regex': query }}, { 'name': True, 'id': True, 'fragment': True, '_id': False })) # TODO(eitan): sort return flask.Response(json.dumps(instructors), mimetype='application/json')
def instructor_lookup(school): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) query = request.args.getlist('instr') instructors = list( c.instructor.find({'id': { '$in': query }}, { 'name': True, 'id': True, 'fragment': True, '_id': False })) # TODO(eitan): sort return flask.Response(json.dumps(instructors), mimetype='application/json')
def ical(school, secret): school = app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': False}) c = mongo.SchoolCollections(school['fragment']) user = util.User(c.user.find_one_or_404({'secret': secret}), school) users.addInstructorCourses(c, user) schedules = user.formatSchedules(timestamps=True) cal = icalendar.Calendar() cal.add('prodid', '-//schdl/schdl//NONSGML v2.0//EN') cal.add('version', '2.0') # TODO(eitan): get timezone from school tz = pytz.timezone('America/New_York') cal.add('x-wr-calname;value=text', 'Schdl: %s' % user.name()) cal.add('x-wr-timezone', 'America/New_York') # TODO(eitan): include link to this schedule cal.add('x-wr-caldesc', 'Generated by Schdl') if schedules: cal.add_component(icalTimezone(tz, schedules)) oneday = datetime.timedelta(days=1) for sch in schedules: term = sch['term'] start = parseDate(term['start']).replace(tzinfo=tz) end = parseDate(term['end']).replace(tzinfo=tz) + oneday for sect in sch['course_sections']: if sect['user_status'] not in ('instructor', 'official'): continue for time in sect['times']: sha1 = hashlib.sha1() sha1.update( json.dumps( dict(school=school['fragment'], user=str(user['_id']), start=start, end=end, sect=sect, time=time))) uid = '%s@%s' % (sha1.hexdigest(), flask.request.host) cal.add_component(makeEvent(start, end, sect, time, uid)) filename = '%s - %s.ics' % (user.school['name'], user.name()) response = flask.Response( response=cal.to_ical(), mimetype=b'text/calendar', ) response.headers.add(b'Content-Description', b'File Transfer') response.headers.add(b'Content-Disposition', b'attachment', filename=filename.encode('utf-8')) return response
def current_user(): # TODO(eitan): optimize! user = flask_login.current_user if user.is_anonymous: return 'null' c = mongo.SchoolCollections(user.school['fragment']) addInstructorCourses(c, user) schedules = user.formatSchedules() roles = {role: True for role in user['roles']} return flask.jsonify(name=user.name(), first=user['first'], middle=user['middle'], last=user['last'], email=user['email'], schedules=schedules, roles=roles, secret=user['secret'])
def subject(school, term, subject): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term = c.term.find_one_or_404( { 'fragment': term, 'subjects.fragment': subject }, { 'fragment': True, 'name': True, 'start': True, 'end': True, 'subjects.$': True, '_id': False, }) out = term['subjects'][0] del term['subjects'] out['term'] = term courses = {segment['id']: [] for segment in out['segments']} courses[None] = [] for course in c.course.find({'subjects.id': out['id']}, util.project_course.mongo()): for subject in course['subjects']: if subject['id'] != out['id']: continue segment = subject.get('segment', None) courses[segment].append(course) del course['subjects'] for segment in courses.itervalues(): segment.sort(key=lambda (course): natsort.natsort_key( course['code'], number_type=None)) segments = [] if courses[None]: segments.append({'courses': courses[None]}) for segment in out['segments']: if courses[segment['id']]: segments.append({ 'name': segment['name'], 'courses': courses[segment['id']] }) out['segments'] = segments return json.jsonify(out)
def push_update(school): school_fragment = school school = app.mongo.db.schools.find_one_or_404( {'fragment': school_fragment}, {'_id': False}) c = mongo.SchoolCollections(school_fragment) try: api_key = flask.request.form['api_key'] except KeyError: return flask.jsonify(reason='no_api_key'), 403 if not app.bcrypt.check_password_hash(school['api_key_hash'], api_key): return flask.jsonify(reason='unrecognized_api_key'), 403 if 'update' not in flask.request.files: return flask.jsonify(reason='update_missing'), 400 _id = add_update(school, flask.request.files['update'], c) return flask.jsonify(id=unicode(_id), status='ok')
def qs_subj(school, term): app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': True}) c = mongo.SchoolCollections(school) term = c.term.find_one_or_404({'fragment': term}, { 'subjects.name': True, 'subjects.searchable_name': True, 'subjects.searchable_abbreviation': True, 'subjects.id': True, '_id': False }) query = util.make_searchable(re.escape(request.args['query'])) subjects = [ subj for subj in term['subjects'] if query in subj['searchable_name'] or query in subj['searchable_abbreviation'] ] for subj in subjects: del subj['searchable_name'], subj['searchable_abbreviation'] return flask.Response(json.dumps(subjects), mimetype='application/json')
def register(school): # If logged in, just return current user if not flask_login.current_user.is_anonymous: return current_user() request = flask.request.json school = app.mongo.db.schools.find_one_or_404({'fragment': school}, {'_id': False}) c = mongo.SchoolCollections(school['fragment']) email_in_use = c.user.find_one({'email': request['email']}) if email_in_use: return flask.jsonify(), 409 # Conflict user = util.User( { 'first': request['first'], 'middle': '', 'last': request['last'], 'email': [request['email']], 'schedules': [], 'secret': util.generate_secret(), 'roles': [], }, school) user.set_password(request['password']) ttl = datetime.timedelta(days=1) secret = util.create_verification(c, 'new_user', user=user.user, ttl=ttl) user['name'] = user.name() msg = flask_mail.Message( "Confirm Your Email Address", # User must have exactly one email address at account creation recipients=user['email'], ) msg.html = flask.render_template( 'email/new_user_verify_email.html', school=school, user=user.user, secret=secret, ) msg.body = flask.render_template( 'email/new_user_verify_email.txt', school=school, user=user.user, secret=secret, ) app.mail.send(msg) return flask.jsonify()
def user_login(school): school = app.mongo.db.schools.find_one({'fragment': school}) c = mongo.SchoolCollections(school['fragment']) email = request.json['email'] password = request.json['password'] user = c.user.find_one({'email': email}) if user is None: return jsonify(reason='noaccount'), 403 if app.bcrypt.check_password_hash(user['passhash'], password): if flask_login.login_user(util.User(user, school)): c.user.update({'_id': user['_id']}, { '$set': { 'last_login': datetime.datetime.utcnow() }, }) return users.current_user() else: return jsonify(reason='unverified'), 403 else: return jsonify(reason='password'), 403
def main(args): parser = argparse.ArgumentParser(description='Process one update.') parser.add_argument('--school', required=True) parser.add_argument('--update_file', type=file) args = parser.parse_args(args) logging.basicConfig(level=logging.INFO) schools = app.mongo.db.schools school = schools.find_one({'fragment': args.school}) if not school: print('School %s not found' % args.school) sys.exit(1) c = mongo.SchoolCollections(args.school) if args.update_file: LOGGER.info('Loading update for %s from %s', school['name'], args.update_file.name) updates.add_update(school, args.update_file, c) results = updates.process_update(school, c) if results is not None: print('%s new, %s updates, %s same, %s deletes' % results)
def test_PUT(self): with test_data.Database.WithTestData() as data: c = mongo.SchoolCollections(data.school['fragment']) user = c.user.find_one({'id': data.user['id']}) self.assertEqual(data.schedule['sections'][0]['status'], user['schedules'][0]['sections'][0]['status']) self.login(data.school['fragment'], data.user['email'], data.user['clearpw']) response = self.app.put( '/api/schedules/%s/%s/%s' % (data.school['fragment'], data.term['fragment'], util.encode_section_id(data.course_section['id'])), data=json.dumps(dict(status='no')), content_type='application/json') self.assertEqual(response.status_code, 200) user = util.User(c.user.find_one({'id': data.user['id']}), data.school) self.assertEqual('no', user['schedules'][0]['sections'][0]['status']) self.assertEqual( { 'name': user.name(), 'first': user['first'], 'middle': user['middle'], 'last': user['last'], 'email': user['email'], 'roles': {}, 'secret': user['secret'], 'schedules': [ user._formatSchedule(util.project_term(data.term), user['schedules'][0]) ], }, json.loads(response.data))