Пример #1
0
def get_admin_campaign():  # rdb_session, user, campaign_id):
    """
    Some non-API related facts.

    # API spec

    Summary: Get admin-level details for a campaign, identified by campaign ID.

    Request model:
        campaign_id:
            type: int64

    Response model name: CampaignDetails
    Response model:
        id:
            type: int64
        name:
            type: string
        rounds:
            type: array
            items:
                type: RoundDetails
        coordinators:
            type: array
            items:
                type: CoordDetails
        url_name:
            type: string

    Errors:
       403: User does not have permission to access requested campaign
       404: Campaign not found

    # End API spec

    More facts
    """
    coord_dao = CoordinatorDAO(rdb_session=rdb_session, user=user)
    campaign = coord_dao.get_campaign(campaign_id)
    if campaign is None:
        raise Forbidden('not a coordinator on this campaign')
    info = {
        'id': campaign.id,
        'name': campaign.name,
        'rounds': [],
        'coords': [u.username for u in campaign.coords]
    }
    for rnd in campaign.rounds:
        info['rounds'].append(get_admin_round(rdb_session, user, rnd.id))

    info['canonical_url_name'] = slugify(info['name'], '-')

    return info
Пример #2
0
def add_coordinator(user_dao, campaign_id, request_dict):
    """
    Summary: -
        Add a new coordinator identified by Wikimedia username to a campaign
        identified by campaign ID

    Request model:
        username

    Response model:
        username
        last_active_date
        campaign_id

    Errors:
       403: User does not have permission to add coordinators

    """
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    new_user_name = request_dict.get('username')
    new_coord = coord_dao.add_coordinator(new_user_name)
    data = {'username': new_coord.username,
            'campaign_id': campaign_id,
            'last_active_date': format_date(new_coord.last_active_date)}
    return {'data': data}
Пример #3
0
def get_campaign(user_dao, campaign_id):
    """
    Summary: Get admin-level details for a campaign, identified by campaign ID.

    Request model:
        campaign_id:
            type: int64

    Response model name: AdminCampaignDetails
    Response model:
        id:
            type: int64
        name:
            type: string
        canonical_url_name:
            type: string
        rounds:
            type: array
            items:
                type: AdminRoundInfo
        coordinators:
            type: array
            items:
                type: CoordDetails

    Errors:
       403: User does not have permission to access requested campaign
       404: Campaign not found
    """
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    campaign = coord_dao.campaign
    if campaign is None:
        raise Forbidden('not a coordinator on this campaign')
    data = campaign.to_details_dict()
    return {'data': data}
Пример #4
0
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
Пример #5
0
def edit_campaign(user_dao, campaign_id, request_dict):
    """
    Summary: Change the settings for a campaign

    Request model:
        campaign_id
        request_dict

    """
    edit_dict = {}
    name = request_dict.get('name')
    if name:
        edit_dict['name'] = name

    is_archived = request_dict.get('is_archived')
    if is_archived is not None:
        edit_dict['is_archived'] = is_archived

    open_date = request_dict.get('open_date')
    if open_date:
        edit_dict['open_date'] = js_isoparse(open_date)
    close_date = request_dict.get('close_date')
    if close_date:
        edit_dict['close_date'] = js_isoparse(close_date)

    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    coord_dao.edit_campaign(edit_dict)
    return {'data': edit_dict}
Пример #6
0
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}
Пример #7
0
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}
Пример #8
0
def remove_coordinator(user_dao, campaign_id, request_dict):
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    username = request_dict.get('username')
    old_coord = coord_dao.remove_coordinator(username)
    data = {'username': username,
            'campaign_id': campaign_id,
            'last_active_date': format_date(old_coord.last_active_date)}
    return {'data': data}
Пример #9
0
def get_campaign_log(user_dao, campaign_id, request_dict):
    limit = request_dict.get('limit', 100)
    offset = request_dict.get('offset', 0)

    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    audit_logs = coord_dao.get_audit_log()
    ret = [a.to_info_dict() for a in audit_logs]
    return {'data': ret}
Пример #10
0
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}
Пример #11
0
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}
Пример #12
0
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}
Пример #13
0
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}
Пример #14
0
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}
Пример #15
0
def finalize_campaign(user_dao, campaign_id):
    # TODO: add some docs
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    last_rnd = coord_dao.campaign.active_round

    if not last_rnd:
        raise InvalidAction('no active rounds')

    if last_rnd.vote_method != 'ranking':
        raise InvalidAction('only ranking rounds can be finalized')

    campaign_summary = coord_dao.finalize_ranking_round(last_rnd.id)
    coord_dao.finalize_campaign()
    return campaign_summary
Пример #16
0
def create_round(user_dao, campaign_id, request_dict):
    """
    Summary: Create a new round

    Request model:
        campaign_id
    """
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)

    rnd_params = _prepare_round_params(coord_dao, request_dict)
    rnd = coord_dao.create_round(**rnd_params)

    data = rnd.to_details_dict()
    data['progress'] = rnd.get_count_map()

    return {'data': data}
Пример #17
0
def get_campaign_log(user_dao, campaign_id, request_dict):
    request_dict = request_dict or dict()
    limit = request_dict.get('limit', 100)
    offset = request_dict.get('offset', 0)
    round_id = request_dict.get('round_id')
    log_id = request_dict.get('id')
    action = request_dict.get('action')

    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    audit_logs = coord_dao.get_audit_log(limit=limit,
                                         offset=offset,
                                         round_id=round_id,
                                         log_id=log_id,
                                         action=action)
    ret = [a.to_info_dict() for a in audit_logs]
    return {'data': ret}
Пример #18
0
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
Пример #19
0
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}
Пример #20
0
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}
Пример #21
0
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}
Пример #22
0
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}
Пример #23
0
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}
Пример #24
0
def get_campaign_report_raw(user_dao, campaign_id):
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    summary = coord_dao.get_campaign_report()
    data = summary.summary
    return {'data': data}
Пример #25
0
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}
Пример #26
0
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}
Пример #27
0
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'}
Пример #28
0
def cancel_campaign(user_dao, campaign_id):
    coord_dao = CoordinatorDAO.from_campaign(user_dao, campaign_id)
    results = coord_dao.cancel_campaign()
    return {'data': results}
Пример #29
0
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}
Пример #30
0
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}