def autodisqualify(user_dao, round_id, request_dict): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) if rnd.status != 'paused': raise InvalidAction('round must be paused to disqualify entries') dq_by_upload_date = request_dict.get('dq_by_upload_date') dq_by_resolution = request_dict.get('dq_by_resolution') dq_by_uploader = request_dict.get('dq_by_uploader') dq_by_filetype = request_dict.get('dq_by_filetype') round_entries = [] if rnd.config.get('dq_by_upload_date') or dq_by_upload_date: dq_upload_date = coord_dao.autodisqualify_by_date(round_id) round_entries += dq_upload_date if rnd.config.get('dq_by_resolution') or dq_by_resolution: dq_resolution = coord_dao.autodisqualify_by_resolution(round_id) round_entries += dq_resolution if rnd.config.get('dq_by_uploader') or dq_by_uploader: dq_uploader = coord_dao.autodisqualify_by_uploader(round_id) round_entries += dq_uploader if rnd.config.get('dq_by_filetype') or dq_by_filetype: dq_filetype = coord_dao.autodisqualify_by_filetype(round_id) round_entries += dq_filetype data = [re.to_dq_details() for re in round_entries] return {'data': data}
def download_results_csv(user_dao, round_id, request_dict): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) now = datetime.datetime.now().isoformat() output_name = 'montage_results-%s-%s.csv' % (slugify(rnd.name, ascii=True), now) # TODO: Confirm round is finalized # raise DoesNotExist('round results not yet finalized') results_by_name = coord_dao.make_vote_table(round_id) output = io.BytesIO() csv_fieldnames = ['filename', 'average'] + [r.username for r in rnd.jurors] csv_writer = unicodecsv.DictWriter(output, fieldnames=csv_fieldnames, restval=None) # na means this entry wasn't assigned csv_writer.writeheader() for filename, ratings in results_by_name.items(): csv_row = {'filename': filename} valid_ratings = [r for r in ratings.values() if type(r) is not str] if valid_ratings: # TODO: catch if there are more than a quorum of votes ratings['average'] = sum(valid_ratings) / len(valid_ratings) else: ratings['average'] = 'na' csv_row.update(ratings) csv_writer.writerow(csv_row) ret = output.getvalue() resp = Response(ret, mimetype='text/csv') resp.mimetype_params['charset'] = 'utf-8' resp.headers['Content-Disposition'] = 'attachment; filename=%s' % output_name return resp
def preview_disqualification(user_dao, round_id): # Let's you see what will get disqualified, without actually # disqualifying any entries coord_dao = CoordinatorDAO.from_round(user_dao, round_id) # TODO: explain each disqualification rnd = coord_dao.get_round(round_id) ret = {'config': rnd.config} by_upload_date = coord_dao.autodisqualify_by_date(round_id, preview=True) ret['by_upload_date'] = [re.entry.to_details_dict(with_uploader=True) for re in by_upload_date] by_resolution = coord_dao.autodisqualify_by_resolution(round_id, preview=True) ret['by_resolution'] = [re.entry.to_details_dict(with_uploader=True) for re in by_resolution] by_uploader = coord_dao.autodisqualify_by_uploader(round_id, preview=True) ret['by_uploader'] = [re.entry.to_details_dict(with_uploader=True) for re in by_uploader] by_filetype = coord_dao.autodisqualify_by_filetype(round_id, preview=True) ret['by_filetype'] = [re.entry.to_details_dict(with_uploader=True) for re in by_filetype] return {'data': ret}
def get_all_flags(user_dao, round_id, request_dict): if not request_dict: request_dict = {} limit = request_dict.get('limit', 10) offset = request_dict.get('offset', 0) coord_dao = CoordinatorDAO.from_round(user_dao, round_id) flags = coord_dao.get_flags(round_id, limit, offset) data = [f.to_details_dict() for f in flags] return {'data': data}
def get_flagged_entries(user_dao, round_id): # TODO: include a limit? coord_dao = CoordinatorDAO.from_round(user_dao, round_id) flagged_entries = coord_dao.get_grouped_flags(round_id) ret = [] for fe in flagged_entries: entry = fe.entry.to_details_dict() entry['flaggings'] = [f.to_details_dict() for f in fe.flaggings] ret.append(entry) return {'data': ret}
def advance_round(user_dao, round_id, request_dict): """Technical there are four possibilities. 1. Advancing from yesno/rating to another yesno/rating 2. Advancing from yesno/rating to ranking 3. Advancing from ranking to yesno/rating 4. Advancing from ranking to another ranking Especially for the first version of Montage, this function will only be written to cover the first two cases. This is because campaigns are designed to end with a single ranking round. typical advancements are: "yesno -> rating -> ranking" or "yesno -> rating -> yesno -> ranking" """ coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) if rnd.vote_method not in ('rating', 'yesno'): raise NotImplementedError() # see docstring above threshold = float(request_dict['threshold']) _next_round_params = request_dict['next_round'] nrp = _prepare_round_params(coord_dao, _next_round_params) if nrp['vote_method'] == 'ranking' \ and len(nrp['jurors']) != nrp.get('quorum'): # TODO: log # (ranking round quorum must match juror count) nrp['quorum'] = len(nrp['jurors']) # TODO: inherit round config from previous round? adv_group = coord_dao.finalize_rating_round(round_id, threshold=threshold) next_rnd = coord_dao.create_round(**nrp) source = 'round(#%s)' % round_id params = {'round': round_id, 'threshold': threshold} coord_dao.add_round_entries(next_rnd.id, adv_group, method=ROUND_METHOD, params=params) # NOTE: disqualifications are not repeated, as they should have # been performed the first round. next_rnd_dict = next_rnd.to_details_dict() next_rnd_dict['progress'] = next_rnd.get_count_map() msg = ( '%s advanced campaign %r (#%s) from %s round "%s" to %s round "%s"' % (user_dao.user.username, rnd.campaign.name, rnd.campaign.id, rnd.vote_method, round_id, next_rnd.vote_method, next_rnd.name)) coord_dao.log_action('advance_round', campaign=rnd.campaign, message=msg) return {'data': next_rnd_dict}
def edit_round(user_dao, round_id, request_dict): """ Summary: Post a new campaign Request model: campaign_name Response model: AdminCampaignDetails """ coord_dao = CoordinatorDAO.from_round(user_dao, round_id) new_val_map = coord_dao.edit_round(round_id, request_dict) return {'data': new_val_map}
def activate_round(user_dao, round_id, request_dict): """ Summary: Set the status of a round to active. Request model: round_id: type: int64 """ coord_dao = CoordinatorDAO.from_round(user_dao, round_id) coord_dao.activate_round(round_id) rnd = coord_dao.get_round(round_id) ret_data = rnd.get_count_map() ret_data['round_id'] = round_id return {'data': ret_data}
def download_round_entries_csv(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) entries = coord_dao.get_round_entries(round_id) entry_infos = [e.to_export_dict() for e in entries] output_name = 'montage_entries-%s.csv' % slugify(rnd.name, ascii=True) output = io.BytesIO() csv_fieldnames = sorted(entry_infos[0].keys()) csv_writer = unicodecsv.DictWriter(output, fieldnames=csv_fieldnames) csv_writer.writeheader() csv_writer.writerows(entry_infos) ret = output.getvalue() resp = Response(ret, mimetype='text/csv') resp.mimetype_params['charset'] = 'utf-8' resp.headers['Content-Disposition'] = 'attachment; filename=%s' % (output_name,) return resp
def get_round_results_preview(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) round_counts = rnd.get_count_map() is_closeable = rnd.check_closability() data = { 'round': rnd.to_info_dict(), 'counts': round_counts, 'is_closeable': is_closeable } if rnd.vote_method in ('yesno', 'rating'): data['ratings'] = coord_dao.get_round_average_rating_map(round_id) try: data['thresholds'] = get_threshold_map(data['ratings']) except: # import pdb;pdb.post_mortem() raise elif rnd.vote_method == 'ranking': if not is_closeable: # TODO: What should this return for ranking rounds? The ranking # round is sorta an all-or-nothing deal, unlike the rating rounds # where you can take a peek at in-progress results # import pdb;pdb.set_trace() return { 'status': 'failure', 'errors': ('cannot preview results of a ranking ' 'round until all ballots are ' 'submitted'), 'data': None } rankings = coord_dao.get_round_ranking_list(round_id) data['rankings'] = [r.to_dict() for r in rankings] else: raise NotImplementedResponse() return {'data': data}
def get_round(user_dao, round_id): """ Summary: Get admin-level details for a round, identified by round ID. Request model: round_id Response model name: AdminRoundDetails Errors: 403: User does not have permission to access requested round 404: Round not found """ coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) rnd_stats = rnd.get_count_map() # entries_info = user_dao.get_entry_info(round_id) # TODO # TODO: joinedload if this generates too many queries data = make_admin_round_details(rnd, rnd_stats) return {'data': data}
def get_round_results_preview(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.get_round(round_id) round_counts = rnd.get_count_map() is_closeable = round_counts['total_open_tasks'] == 0 data = { 'round': rnd.to_info_dict(), 'counts': round_counts, 'is_closeable': is_closeable } if rnd.vote_method in ('yesno', 'rating'): data['ratings'] = coord_dao.get_round_average_rating_map(round_id) try: data['thresholds'] = get_threshold_map(data['ratings']) except: # import pdb;pdb.post_mortem() raise elif rnd.vote_method == 'ranking': if not is_closeable: # TODO: What should this return for ranking rounds? The ranking # round is sorta an all-or-nothing deal, unlike the rating rounds # where you can take a peek at in-progress results # import pdb;pdb.set_trace() raise InvalidAction('round must be closeable to preview results') rankings = coord_dao.get_round_ranking_list(round_id) data['rankings'] = [r.to_dict() for r in rankings] else: raise NotImplementedError() return {'data': data}
def get_disqualified(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) round_entries = coord_dao.get_disqualified(round_id) data = [re.to_dq_details() for re in round_entries] return {'data': data}
def disqualify_entry(user_dao, round_id, entry_id, request_dict): if not request_dict: request_dict = {} reason = request_dict.get('reason') coord_dao = CoordinatorDAO.from_round(user_dao, round_id) coord_dao.disqualify(round_id, entry_id, reason)
def requalify_entry(user_dao, round_id, entry_id, request_dict): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) coord_dao.requalify(round_id, entry_id)
def import_entries(user_dao, round_id, request_dict): """ Summary: Load entries into a round via one of four import methods Request model: - round_id (in path) - import_method: - gistcsv - category - round - selected - gist_url (if import_method=gistcsv) - category (if import_method=category) - threshold (if import_method=round) - file_names (if import_method=selected) Response model name: - data: - round_id - new_entry_count - new_round_entry_count - total_entries - status: success or failure - errors: description of the failure (if any) - warnings: possible problems to alert the user - empty import (no entries) - duplicate import (no new entries) - all disqualified """ coord_dao = CoordinatorDAO.from_round(user_dao, round_id) import_method = request_dict['import_method'] # loader warnings import_warnings = list() if import_method == 'csv' or import_method == 'gistcsv': if import_method == 'gistcsv': csv_url = request_dict['gist_url'] else: csv_url = request_dict['csv_url'] entries, warnings = coord_dao.add_entries_from_csv(round_id, csv_url) params = {'csv_url': csv_url} if warnings: msg = 'unable to load {} files ({!r})'.format(len(warnings), warnings) import_warnings.append(msg) elif import_method == CATEGORY_METHOD: cat_name = request_dict['category'] entries = coord_dao.add_entries_from_cat(round_id, cat_name) params = {'category': cat_name} elif import_method == ROUND_METHOD: threshold = request_dict['threshold'] prev_round_id = request_dict['previous_round_id'] entries = coord_dao.get_rating_advancing_group(prev_round_id, threshold) params = {'threshold': threshold, 'round_id': prev_round_id} elif import_method == SELECTED_METHOD: file_names = request_dict['file_names'] entries, warnings = coord_dao.add_entries_by_name(round_id, file_names) if warnings: formatted_warnings = '\n'.join([ '- {}'.format(warning) for warning in warnings ]) msg = 'unable to load {} files:\n{}'.format(len(warnings), formatted_warnings) import_warnings.append({'import issues', msg}) params = {'file_names': file_names} else: raise NotImplementedResponse() new_entry_stats = coord_dao.add_round_entries(round_id, entries, method=import_method, params=params) new_entry_stats['warnings'] = import_warnings if not entries: new_entry_stats['warnings'].append({'empty import': 'no entries imported'}) elif not new_entry_stats.get('new_entry_count'): new_entry_stats['warnings'].append({'duplicate import': 'no new entries imported'}) # automatically disqualify entries based on round config auto_dq = autodisqualify(user_dao, round_id, request_dict={}) new_entry_stats['disqualified'] = auto_dq['data'] if len(new_entry_stats['disqualified']) >= len(entries): new_entry_stats['warnings'].append({'all disqualified': 'all entries disqualified by round settings'}) return {'data': new_entry_stats}
def get_results(user_dao, round_id, request_dict): # TODO: Docs coord_dao = CoordinatorDAO.from_round(user_dao, round_id) results_by_name = coord_dao.make_vote_table(round_id) return {'data': results_by_name}
def cancel_round(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) rnd = coord_dao.cancel_round(round_id) stats = rnd.get_count_map() return {'data': stats}
def get_round_reviews(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) entries = coord_dao.get_reviews_table(round_id) entry_infos = [e.to_details_dict() for e in entries] return {'data': entry_infos}
def get_round_entries(user_dao, round_id): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) entries = coord_dao.get_round_entries(round_id) entry_infos = [e.to_export_dict() for e in entries] return {'file_infos': entry_infos}
def pause_round(user_dao, round_id, request_dict): coord_dao = CoordinatorDAO.from_round(user_dao, round_id) coord_dao.pause_round(round_id) return {'data': 'paused'}