diff --git a/.gitignore b/.gitignore index 164f9316c..0e56cdd36 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ mittab/backups/ # venv venv/ + +# no idea/pycharm +/.idea +.idea diff --git a/mittab/apps/tab/debater_views.py b/mittab/apps/tab/debater_views.py index 0b8cb4621..e5a96665e 100644 --- a/mittab/apps/tab/debater_views.py +++ b/mittab/apps/tab/debater_views.py @@ -1,11 +1,13 @@ +import pprint +import time + from django.shortcuts import render_to_response from django.template import RequestContext -from django.http import Http404,HttpResponse,HttpResponseRedirect from django.contrib.auth.decorators import permission_required +from django.db import connection + from forms import DebaterForm -from errors import * from models import * - from mittab.libs import tab_logic, errors def view_debaters(request): @@ -111,24 +113,48 @@ def rank_debaters_ajax(request): {'title': "Debater Rankings"}, context_instance=RequestContext(request)) + def rank_debaters(request): - speakers = tab_logic.rank_speakers() - debaters = [(s, - tab_logic.tot_speaks_deb(s), - tab_logic.tot_ranks_deb(s), - tab_logic.deb_team(s)) for s in speakers] - - nov_speakers = tab_logic.rank_nov_speakers() - nov_debaters = [(s, - tab_logic.tot_speaks_deb(s), - tab_logic.tot_ranks_deb(s), - tab_logic.deb_team(s)) for s in nov_speakers] - - return render_to_response('rank_debaters_component.html', - {'debaters': debaters, - 'nov_debaters' : nov_debaters, - 'title': "Speaker Rankings"}, - context_instance=RequestContext(request)) + start_ms = int(round(time.time() * 1000)) + + # old method + # speakers = tab_logic.rank_speakers() + # debaters = [(s, + # tab_logic.tot_speaks_deb(s), + # tab_logic.tot_ranks_deb(s), + # tab_logic.deb_team(s)) for s in speakers] + # + # nov_speakers = tab_logic.rank_nov_speakers() + # nov_debaters = [(s, + # tab_logic.tot_speaks_deb(s), + # tab_logic.tot_ranks_deb(s), + # tab_logic.deb_team(s)) for s in nov_speakers] + + debater_scores = tab_logic.get_debater_scores() + print('got debater information') + + scores = sorted(debater_scores, key=lambda d: d.create_scoring_tuple()) + debaters = [(ds.speaker, + ds.tot_speaks, + ds.tot_ranks, + tab_logic.deb_team(ds.speaker)) for ds in scores] + print('got the visualised speaks and ranks') + + # since removing entries has no effect on ordinal rank... just remove them + nov_debaters = [t for t in debaters if t[0].novice_status == Debater.NOVICE] # save on mem allocation too + print('rendering') + + end_ms = int(round(time.time() * 1000)) + print('derivation took {} ms'.format(end_ms - start_ms)) + + # print('made the following queries:') + # pprint.pprint(['\t' + str(s) for s in connection.queries]) + + return render_to_response('rank_debaters_component.html', + {'debaters': debaters, + 'nov_debaters': nov_debaters, + 'title': "Speaker Rankings"}, + context_instance=RequestContext(request)) diff --git a/mittab/apps/tab/export_views.py b/mittab/apps/tab/export_views.py new file mode 100644 index 000000000..c38ccc7fc --- /dev/null +++ b/mittab/apps/tab/export_views.py @@ -0,0 +1,60 @@ +from django.contrib.auth.decorators import permission_required +from django.http import HttpResponse +from django.shortcuts import render + +from mittab.libs.data_import import export_xls_files + + +def export_xls_portal(request): + """ Export portal, displays links to be used """ + return render(request, 'export_links.html') + + +@permission_required('tab.tab_settings.can_change', login_url="/403/") +def export_team_xls(request): + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=mittab-teams.xls' + + book = export_xls_files.export_teams() + book.save(response) + return response + + +@permission_required('tab.tab_settings.can_change', login_url="/403/") +def export_judge_xls(request): + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=mittab-judges.xls' + + book = export_xls_files.export_judges() + book.save(response) + return response + + +@permission_required('tab.tab_settings.can_change', login_url="/403/") +def export_room_xls(request): + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=mittab-rooms.xls' + + book = export_xls_files.export_rooms() + book.save(response) + return response + + +@permission_required('tab.tab_settings.can_change', login_url="/403/") +def export_team_stats_xls(request): + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=mittab-team-stats.xls' + + book = export_xls_files.export_team_stats() + book.save(response) + return response + + +@permission_required('tab.tab_settings.can_change', login_url="/403/") +def export_debater_stats_xls(request): + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=mittab-debater-stats.xls' + + book = export_xls_files.export_debater_stats() + book.save(response) + return response diff --git a/mittab/apps/tab/forms.py b/mittab/apps/tab/forms.py index c8e1043ad..32f28c366 100644 --- a/mittab/apps/tab/forms.py +++ b/mittab/apps/tab/forms.py @@ -8,16 +8,27 @@ from django.core.exceptions import ValidationError from decimal import Decimal +from django.utils.safestring import mark_safe + from models import * from mittab.libs import errors class UploadBackupForm(forms.Form): file = forms.FileField(label="Your Backup File") + class UploadDataForm(forms.Form): - team_file = forms.FileField(label="Teams Data File", required=False) - judge_file = forms.FileField(label="Judge Data File", required=False) - room_file = forms.FileField(label="Room Data File", required=False) + """Creates the form for uploading files.""" + team_file = forms.FileField(label=mark_safe('Teams Data File
(Name, School, Seed [full, half, free, ' + 'none], D1 name, D1 status [varsity, novice, nov, n], D1 phone, ' + 'D1 prov,
D2 name, D2 [varsity, novice, nov, n], D2 phone, ' + 'D2 prov) Note: Overwrites data for duplicate teams
'), + required=False) + judge_file = forms.FileField(label=mark_safe('Judge Data File
(name, rank, phone #, provider, ' + 'school) Note: Overwrites data for duplicate judges'), + required=False) + room_file = forms.FileField(label=mark_safe('Room Data File
(name, rank) Note: Overwrites data for ' + 'duplicate rooms'), required=False) class SchoolForm(forms.ModelForm): diff --git a/mittab/apps/tab/pairing_views.py b/mittab/apps/tab/pairing_views.py index 354ac47da..c4806e71a 100644 --- a/mittab/apps/tab/pairing_views.py +++ b/mittab/apps/tab/pairing_views.py @@ -267,37 +267,45 @@ def view_status(request): current_round_number = TabSettings.objects.get(key="cur_round").value - 1 return view_round(request, current_round_number) -def view_round(request, round_number, errors = None): - if errors is None: - errors = [] + +def view_round(request, round_number, errors=[]): + start_time = start_ms = int(round(time.time() * 1000)) + valid_pairing, byes = True, [] - round_pairing = list(Round.objects.filter(round_number=round_number)) + round_pairing = list(Round.objects.select_related('gov_team', 'opp_team') + .prefetch_related('gov_team__debaters__roundstats_set', + 'opp_team__debaters__roundstats_set', + 'judges') + .filter(round_number=round_number)) random.seed(1337) random.shuffle(round_pairing) - round_pairing.sort(key = lambda x: tab_logic.team_comp(x, round_number), - reverse = True) + round_pairing.sort(key=lambda x: tab_logic.team_comp(x, round_number), + reverse=True) - #For the template since we can't pass in something nicer like a hash + # For the template since we can't pass in something nicer like a hash round_info = [pair for pair in round_pairing] - paired_teams = [team.gov_team for team in round_pairing] + [team.opp_team for team in round_pairing] + paired_teams = [p_round.gov_team for p_round in round_pairing] + [p_round.opp_team for p_round in round_pairing] n_over_two = Team.objects.filter(checked_in=True).count() / 2 valid_pairing = len(round_pairing) >= n_over_two or round_number == 0 + for present_team in Team.objects.filter(checked_in=True): if not (present_team in paired_teams): - errors.append("%s was not in the pairing" % (present_team)) + errors.append("%s was not in the pairing" % present_team) byes.append(present_team) + pairing_exists = len(round_pairing) > 0 pairing_released = TabSettings.get("pairing_released", 0) == 1 judges_assigned = all((r.judges.count() > 0 for r in round_info)) - excluded_judges = Judge.objects.exclude(judges__round_number=round_number).filter(checkin__round_number = round_number) - non_checkins = Judge.objects.exclude(judges__round_number=round_number).exclude(checkin__round_number = round_number) + + excluded_judges = Judge.objects.exclude(judges__round_number=round_number).filter(checkin__round_number=round_number) + non_checkins = Judge.objects.exclude(judges__round_number=round_number).exclude(checkin__round_number=round_number) available_rooms = Room.objects.exclude(round__round_number=round_number).exclude(rank=0) size = max(map(len, [excluded_judges, non_checkins, byes])) # The minimum rank you want to warn on warning = 5 - judge_slots = [1,2,3] + judge_slots = [1, 2, 3] # A seemingly complex one liner to do a fairly simple thing # basically this generates the table that the HTML will display such that the output looks like: @@ -306,11 +314,15 @@ def view_round(request, round_number, errors = None): # [ Team2][ CJudge2 ][ Judge2 ] # [ ][ CJudge3 ][ Judge3 ] # [ ][ ][ Judge4 ] - excluded_people = zip(*map( lambda x: x+[""]*(size-len(x)), [list(byes), list(excluded_judges), list(non_checkins), list(available_rooms)])) + excluded_people = zip(*map(lambda x: x+[""]*(size - len(x)), [list(byes), list(excluded_judges), list(non_checkins), + list(available_rooms)])) + + end_time = int(round(time.time() * 1000)) + print('took {} ms to render main pairings (ajax not included)'.format(end_time - start_time)) return render_to_response('pairing_control.html', - locals(), - context_instance=RequestContext(request)) + locals(), + context_instance=RequestContext(request)) def alternative_judges(request, round_id, judge_id=None): round_obj = Round.objects.get(id=int(round_id)) diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 92ff51f1a..10761ab3d 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -1,4 +1,4 @@ -from datetime import datetime +import time from django.shortcuts import render_to_response from django.template import RequestContext @@ -24,12 +24,12 @@ def flags(team): for t in Team.objects.all().order_by("name")] all_flags = [[TabFlags.TEAM_CHECKED_IN, TabFlags.TEAM_NOT_CHECKED_IN]] filters, symbol_text = TabFlags.get_filters_and_symbols(all_flags) - return render_to_response('list_data.html', + return render_to_response('list_data.html', {'item_type':'team', 'title': "Viewing All Teams", 'item_list': c_teams, 'filters': filters, - 'symbol_text': symbol_text}, + 'symbol_text': symbol_text}, context_instance=RequestContext(request)) def view_team(request, team_id): @@ -45,10 +45,10 @@ def view_team(request, team_id): stats.append(("Been Pullup", tab_logic.pull_up_count(team))) stats.append(("Hit Pullup", tab_logic.hit_pull_up_count(team))) except Team.DoesNotExist: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "View Team", 'error_name': str(team_id), - 'error_info':"No such Team"}, + 'error_info':"No such Team"}, context_instance=RequestContext(request)) if request.method == 'POST': form = TeamForm(request.POST,instance=team) @@ -56,30 +56,30 @@ def view_team(request, team_id): try: form.save() except ValueError: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Team", 'error_name': "["+form.cleaned_data['name']+"]", - 'error_info':"Team name cannot be validated, most likely a non-existent team"}, + 'error_info':"Team name cannot be validated, most likely a non-existent team"}, context_instance=RequestContext(request)) - return render_to_response('thanks.html', + return render_to_response('thanks.html', {'data_type': "Team", - 'data_name': "["+form.cleaned_data['name']+"]"}, + 'data_name': "["+form.cleaned_data['name']+"]"}, context_instance=RequestContext(request)) else: form = TeamForm(instance=team) links = [('/team/'+str(team_id)+'/scratches/view/',u'Scratches for {}'.format(team.name), False)] for deb in team.debaters.all(): links.append(('/debater/'+str(deb.id)+'/', "View %s" % deb.name, False)) - return render_to_response('data_entry.html', + return render_to_response('data_entry.html', {'title':"Viewing Team: %s"%(team.name), 'form': form, 'links': links, 'team_obj':team, - 'team_stats':stats}, + 'team_stats':stats}, context_instance=RequestContext(request)) - return render_to_response('data_entry.html', - {'form': form}, + return render_to_response('data_entry.html', + {'form': form}, context_instance=RequestContext(request)) def enter_team(request): @@ -89,15 +89,15 @@ def enter_team(request): try: team = form.save() except ValueError: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Team",'error_name': "["+form.cleaned_data['name']+"]", - 'error_info':"Team name cannot be validated, most likely a duplicate school"}, + 'error_info':"Team name cannot be validated, most likely a duplicate school"}, context_instance=RequestContext(request)) num_forms = form.cleaned_data['number_scratches'] if num_forms > 0: return HttpResponseRedirect('/team/'+str(team.pk)+'/scratches/add/'+str(num_forms)) else: - return render_to_response('thanks.html', + return render_to_response('thanks.html', {'data_type': "Team", 'data_name': u'[{}]'.format(team.name), 'data_modification': 'CREATED', @@ -109,42 +109,42 @@ def enter_team(request): return render_to_response('data_entry.html', {'form': form, 'title': "Create Team"}, context_instance=RequestContext(request)) - -@permission_required('tab.team.can_delete', login_url="/403/") + +@permission_required('tab.team.can_delete', login_url="/403/") def delete_team(request, team_id): team_id = int(team_id) try : team = Team.objects.get(pk=team_id) team.delete() except Team.DoesNotExist: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Team", 'error_name': str(team_id), - 'error_info':"Team does not exist"}, + 'error_info':"Team does not exist"}, context_instance=RequestContext(request)) - return render_to_response('thanks.html', + return render_to_response('thanks.html', {'data_type': "Team", 'data_name': "["+str(team_id)+"]", - 'data_modification': 'DELETED'}, + 'data_modification': 'DELETED'}, context_instance=RequestContext(request)) def add_scratches(request, team_id, number_scratches): try: team_id,number_scratches = int(team_id),int(number_scratches) except ValueError: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Scratch",'error_name': "Data Entry", - 'error_info':"I require INTEGERS!"}, + 'error_info':"I require INTEGERS!"}, context_instance=RequestContext(request)) try: team = Team.objects.get(pk=team_id) except Team.DoesNotExist: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Add Scratches for Team", 'error_name': str(team_id), - 'error_info':"No such Team"}, + 'error_info':"No such Team"}, context_instance=RequestContext(request)) - + if request.method == 'POST': forms = [ScratchForm(request.POST, prefix=str(i)) for i in range(1,number_scratches+1)] all_good = True @@ -153,26 +153,26 @@ def add_scratches(request, team_id, number_scratches): if all_good: for form in forms: form.save() - return render_to_response('thanks.html', + return render_to_response('thanks.html', {'data_type': "Scratches for team", 'data_name': "["+str(team_id)+"]", 'data_modification': "CREATED"}, - context_instance=RequestContext(request)) + context_instance=RequestContext(request)) else: forms = [ScratchForm(prefix=str(i), initial={'team':team_id,'scratch_type':0}) for i in range(1,number_scratches+1)] - return render_to_response('data_entry_multiple.html', + return render_to_response('data_entry_multiple.html', {'forms': zip(forms,[None]*len(forms)), 'data_type':'Scratch', - 'title':"Adding Scratch(es) for %s"%(team.name)}, + 'title':"Adding Scratch(es) for %s"%(team.name)}, context_instance=RequestContext(request)) - + def view_scratches(request, team_id): try: team_id = int(team_id) except ValueError: - return render_to_response('error.html', + return render_to_response('error.html', {'error_type': "Scratch",'error_name': "Delete", - 'error_info':"I require INTEGERS!"}, + 'error_info':"I require INTEGERS!"}, context_instance=RequestContext(request)) scratches = Scratch.objects.filter(team=team_id) number_scratches = len(scratches) @@ -185,20 +185,20 @@ def view_scratches(request, team_id): if all_good: for form in forms: form.save() - return render_to_response('thanks.html', + return render_to_response('thanks.html', {'data_type': "Scratches for team", 'data_name': "["+str(team_id)+"]", 'data_modification': "EDITED"}, - context_instance=RequestContext(request)) + context_instance=RequestContext(request)) else: forms = [ScratchForm(prefix=str(i), instance=scratches[i-1]) for i in range(1,len(scratches)+1)] delete_links = ["/team/"+str(team_id)+"/scratches/delete/"+str(scratches[i].id) for i in range(len(scratches))] links = [('/team/'+str(team_id)+'/scratches/add/1/','Add Scratch', False)] - return render_to_response('data_entry_multiple.html', + return render_to_response('data_entry_multiple.html', {'forms': zip(forms,delete_links), 'data_type':'Scratch', 'links':links, - 'title':"Viewing Scratch Information for %s"%(team.name)}, + 'title':"Viewing Scratch Information for %s"%(team.name)}, context_instance=RequestContext(request)) @permission_required('tab.tab_settings.can_change', login_url="/403/") @@ -314,10 +314,10 @@ def tab_card(request, team_id): except: bye_round = None - #Duplicates Debater 1 for display if Ironman team + #Duplicates Debater 1 for display if Ironman team if (iron_man): d2 = d1 - return render_to_response('tab_card.html', + return render_to_response('tab_card.html', {'team_name': team.name, 'team_school': team.school, 'debater_1': d1.name, @@ -339,44 +339,69 @@ def rank_teams_ajax(request): {'title': "Team Rankings"}, context_instance=RequestContext(request)) + def rank_teams(request): - print "starting rankings: ", datetime.now() - ranked_teams = tab_logic.rank_teams() - print "Got ranked teams" - teams = [(team, - tab_logic.tot_wins(team), - tab_logic.tot_speaks(team), - tab_logic.tot_ranks(team)) - for team in ranked_teams] - - print "started novice rankings: ", datetime.now() - ranked_novice_teams = tab_logic.rank_nov_teams() - nov_teams = [(team, - tab_logic.tot_wins(team), - tab_logic.tot_speaks(team), - tab_logic.tot_ranks(team)) - for team in ranked_novice_teams] - - print "Got ranked novice teams" + start_ms = int(round(time.time() * 1000)) + + # ranked_teams = tab_logic.rank_teams() + # print "Got ranked teams" + # teams = [(team, + # tab_logic.tot_wins(team), + # tab_logic.tot_speaks(team), + # tab_logic.tot_ranks(team)) + # for team in ranked_teams] + # + # ranked_novice_teams = tab_logic.rank_nov_teams() + # nov_teams = [(team, + # tab_logic.tot_wins(team), + # tab_logic.tot_speaks(team), + # tab_logic.tot_ranks(team)) + # for team in ranked_novice_teams] + # + # print "Got ranked novice teams" + + team_scores = tab_logic.get_team_scores() + print('got team information') + + ranked_teams = sorted(team_scores, key=lambda d: d.create_scoring_tuple()) + teams = [(ts.team, + ts.wins, + ts.tot_speaks, + ts.tot_ranks) for ts in ranked_teams] + print('got the visualised information') + + # since removing entries has no effect on ordinal rank... just remove them + # all members must be novices to be a novice team + nov_teams = [t for t in teams if all(s.novice_status == Debater.NOVICE for s in t[0].debaters.all())] + print('rendering') + + end_ms = int(round(time.time() * 1000)) + print('team derivation took {} ms'.format(end_ms - start_ms)) + return render_to_response('rank_teams_component.html', - {'varsity': teams, - 'novice': nov_teams, - 'title': "Team Rankings"}, + {'varsity': teams, + 'novice': nov_teams, + 'title': "Team Rankings"}, context_instance=RequestContext(request)) + def team_stats(request, team_id): team_id = int(team_id) try: - team = Team.objects.get(pk=team_id) - stats = {} - stats["seed"] = Team.get_seed_display(team).split(" ")[0] - stats["wins"] = tab_logic.tot_wins(team) - stats["total_speaks"] = tab_logic.tot_speaks(team) - stats["govs"] = tab_logic.num_govs(team) - stats["opps"] = tab_logic.num_opps(team) - data = {'success': True, 'result':stats} + team = Team.objects.prefetch_related('gov_team', 'opp_team', 'debaters__roundstats_set').get(pk=team_id) + + stats = { + "seed": Team.get_seed_display(team).split(" ")[0], + "wins": tab_logic.tot_wins(team), + "total_speaks": tab_logic.tot_speaks(team), + "govs": tab_logic.num_govs(team), + "opps": tab_logic.num_opps(team) + } + data = {'success': True, 'result': stats} + except Team.DoesNotExist: data = {'success': False} + return JsonResponse(data) diff --git a/mittab/apps/tab/templates/403.html b/mittab/apps/tab/templates/403.html index 3b869cf75..74668a3ef 100644 --- a/mittab/apps/tab/templates/403.html +++ b/mittab/apps/tab/templates/403.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}ERROR!{% endblock %} +{% block title %}403 Forbidden{% endblock %} {% block content %}
diff --git a/mittab/apps/tab/templates/base.html b/mittab/apps/tab/templates/base.html index 9bf5292e8..c52540a79 100644 --- a/mittab/apps/tab/templates/base.html +++ b/mittab/apps/tab/templates/base.html @@ -22,7 +22,7 @@
-
MIT-TAB
+
MIT-TAB
{% if not no_navigation %} {% include "navigation.html" %} diff --git a/mittab/apps/tab/templates/data_entry.html b/mittab/apps/tab/templates/data_entry.html index 31c2e9210..ee10d787d 100644 --- a/mittab/apps/tab/templates/data_entry.html +++ b/mittab/apps/tab/templates/data_entry.html @@ -64,7 +64,7 @@ {%endif%}
-
+
diff --git a/mittab/apps/tab/templates/error.html b/mittab/apps/tab/templates/error.html index d0f64b408..5ee0271e2 100644 --- a/mittab/apps/tab/templates/error.html +++ b/mittab/apps/tab/templates/error.html @@ -3,7 +3,7 @@ {% block title %}ERROR!{% endblock %} {% block content %} -

{{ error_type }} {{ error_name }} ecountered an error!

+

{{ error_type }} {{ error_name }} encountered an error!

Reason for Error: {{error_info}}

{% endblock %} \ No newline at end of file diff --git a/mittab/apps/tab/templates/export_links.html b/mittab/apps/tab/templates/export_links.html new file mode 100644 index 000000000..6b3d3464c --- /dev/null +++ b/mittab/apps/tab/templates/export_links.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block title %}Export XLS file{% endblock %} +{% block banner %}Export XLS file{% endblock %} +{% block content %} + +

Download links to dynamically generated and up-to-date XLS files each of which contains team, judge, and room + properties. These files are formatted so they can easily be re-imported using the XLS + File Import function. Note that when importing, the data in the Excel sheet will overwrite the data on + MIT-TAB's database.

+
+ +
+ +
+ +

These links can be used to export team and speaker statistics which may be useful in calculating the break or + speaker awards if your tournament uses a custom tabulation policy which differs from the standard ranking + provided by MIT-TAB, which uses total speaks, ranks; single-adjusted speaks, ranks; double-adjusted speaks, + ranks; opp-strength; coin-flip.

+
+ +
+{% endblock %} \ No newline at end of file diff --git a/mittab/apps/tab/templates/navigation.html b/mittab/apps/tab/templates/navigation.html index af2b8acf3..496622911 100644 --- a/mittab/apps/tab/templates/navigation.html +++ b/mittab/apps/tab/templates/navigation.html @@ -51,7 +51,8 @@ diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index b7c11fed6..96dceae58 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -272,47 +272,48 @@ def view_scratches(request): 'title': "Viewing All Scratches for Teams", 'item_list':c_scratches}, context_instance=RequestContext(request)) + def upload_data(request): if request.method == 'POST': - form = UploadDataForm(request.POST, request.FILES) - if form.is_valid(): - judge_errors = room_errors = team_errors = [] - importName = '' - results = '' + form = UploadDataForm(request.POST, request.FILES) + if form.is_valid(): + import_name = '' + results = '' - if 'team_file' in request.FILES: - team_errors = import_teams.import_teams(request.FILES['team_file']) - importName += request.FILES['team_file'].name + ' ' - if len(team_errors) > 0: - results += 'Team Import Errors (Please Check These Manually):\n' - for e in team_errors: - results += ' ' + e + '\n' - if 'judge_file' in request.FILES: - judge_errors = import_judges.import_judges(request.FILES['judge_file']) - importName += request.FILES['judge_file'].name + ' ' - if len(judge_errors) > 0: - results += 'Judge Import Errors (Please Check These Manually):\n' - for e in judge_errors: - results += ' ' + e + '\n' - if 'room_file' in request.FILES: - room_errors = import_rooms.import_rooms(request.FILES['room_file']) - importName += request.FILES['room_file'].name + ' ' - if len(room_errors) > 0: - results += 'Room Import Errors (Please Check These Manually):\n' - for e in room_errors: - results += ' ' + e + '\n' + if 'team_file' in request.FILES: + team_errors = import_teams.import_teams(request.FILES['team_file']) + import_name += request.FILES['team_file'].name + ' ' + if len(team_errors) > 0: + results += 'Team Import Errors (Please Check These Manually):\n' + for e in team_errors: + results += ' ' + e + '\n' - return render_to_response('thanks.html', - {'data_type': "Database data", - 'data_name': importName, - 'data_modification': "INPUT", - 'results': True, - 'data_results': results}, - context_instance=RequestContext(request)) + if 'judge_file' in request.FILES: + judge_errors = import_judges.import_judges(request.FILES['judge_file']) + import_name += request.FILES['judge_file'].name + ' ' + if len(judge_errors) > 0: + results += 'Judge Import Errors (Please Check These Manually):\n' + for e in judge_errors: + results += ' ' + e + '\n' + + if 'room_file' in request.FILES: + room_errors = import_rooms.import_rooms(request.FILES['room_file']) + import_name += request.FILES['room_file'].name + ' ' + if len(room_errors) > 0: + results += 'Room Import Errors (Please Check These Manually):\n' + for e in room_errors: + results += ' ' + e + '\n' + + return render_to_response('thanks.html', + {'data_type': "Database data", + 'data_name': import_name, + 'data_modification': "INPUT", + 'results': True, + 'data_results': results}, + context_instance=RequestContext(request)) else: - form = UploadDataForm() - return render_to_response('data_entry.html', + form = UploadDataForm() + return render_to_response('data_entry.html', {'form': form, - 'title': 'Upload Input Files'}, - context_instance=RequestContext(request)) - + 'title': 'Upload Input Files'}, + context_instance=RequestContext(request)) diff --git a/mittab/libs/data_import/export_xls_files.py b/mittab/libs/data_import/export_xls_files.py new file mode 100644 index 000000000..eb047a3c4 --- /dev/null +++ b/mittab/libs/data_import/export_xls_files.py @@ -0,0 +1,266 @@ +# data stuff +from xlwt import Workbook +import pandas as pd + +from mittab.apps.tab.models import Team, Judge, Room, Debater +from mittab.libs import tab_logic + + +def _vn_status_to_str(debater_vn): + """Creates varsity-novice status string from the integer pseudo-enum used by the model""" + return next((description for i, description in Debater.NOVICE_CHOICES if i == debater_vn), + 'NO STATUS') + + +def _seed_to_str(seed_int): + """Creates seed status string from the integer pseudo-enum used by the model""" + return next((description for i, description in Team.SEED_CHOICES if i == seed_int), + 'NO SEED') + + +def export_teams_df(): + """Exports teams as a new XLS file which is then streamed to an HTTP response. This file should always be + cross-compatible with the parsing system used in the import_teams file. """ + + entries = [] + for team in Team.objects.all(): + entry = { + 'team_name': team.name, + 'team_school': team.school.name, + 'team_seed': _seed_to_str(team.seed) + } + + for debater_count, debater in enumerate(team.debaters.all()): + root = 'debater_{}_'.format(debater_count + 1) + entry[root + 'name'] = debater.name + entry[root + 'status'] = _vn_status_to_str(debater.novice_status) + + entries.append(entry) + + return pd.DataFrame(entries) + + +def export_teams(): + """Exports teams as a new XLS file which is then streamed to an HTTP response. This file should always be + cross-compatible with the parsing system used in the import_teams file. """ + + book = Workbook('utf-8') + sheet = book.add_sheet('Teams') + + # write headers + headers = ['team_name', 'team_school', 'team_seed', 'team_debater_1_name', 'team_debater_1_status', + 'team_debater_2_name', 'team_debater_2_status'] + for i, header in enumerate(headers): + sheet.write(0, i, header) + + # write rows + for i, team in enumerate(Team.objects.all()): + row = i + 1 + + name = team.name + school = team.school.name + + # convert seed + # seed = 0 if unseeded, seed = 1 if free seed, seed = 2 if half seed, seed = 3 if full seed + seed = _seed_to_str(team.seed) + + debaters = team.debaters.all() + deb1_name = debaters[0].name + deb1_status = _vn_status_to_str(debaters[0].novice_status) # 0 = Varsity, 1 = Novice + + sheet.write(row, 0, name) + sheet.write(row, 1, school) + sheet.write(row, 2, seed) + + sheet.write(row, 3, deb1_name) + sheet.write(row, 4, deb1_status) + + if len(debaters) > 1: + # implicitly caps to two debater teams + deb2_name = debaters[1].name + deb2_status = _vn_status_to_str(debaters[1].novice_status) + + sheet.write(row, 5, deb2_name) + sheet.write(row, 6, deb2_status) + + return book + + +def export_judges_df(): + entries = [] + for judge in Judge.objects.all(): + entry = { + 'judge_name': judge.name, + 'judge_rank': judge.rank + } + + for judge_i, school in enumerate(judge.schools.all()): + entry['judge_school_{}'.format(judge_i + 1)] = school.name + + entries.append(entry) + + df = pd.DataFrame(entries) + return df + + +def export_judges(): + """Exports judges to a XLS file which is then streamed to the recipient. This method should always be + cross-compatible with the import_judges file. """ + book = Workbook('utf-8') + sheet = book.add_sheet('Judges') + + # 0 name, 1 rank, 2+ schools + headers = ['judge_name', 'judge_rank', 'judge_schools'] + for i, header in enumerate(headers): + sheet.write(0, i, header) + + for i, judge in enumerate(Judge.objects.all()): + row = i + 1 + + name = judge.name + rank = judge.rank + schools = judge.schools.all() + + sheet.write(row, 0, name) + sheet.write(row, 1, rank) + + for j, school in enumerate(schools): # iterate through other school affiliations + sheet.write(row, j + 2, school.name) + + return book + + +def export_rooms_df(): + entries = [] + for room in Room.objects.all(): + entry = { + 'room_name': room.name, + 'room_rank': room.rank + } + entries.append(entry) + + return pd.DataFrame(entries) + + +def export_rooms(): + """Exports rooms to an XLS file which is then streamed to the recipient. This method should always be + cross-compatible with the import_rooms file. """ + book = Workbook('utf-8') + sheet = book.add_sheet('Rooms') + + # 0 name, 1 rank + headers = ['room_name', 'room_rank'] + for i, header in enumerate(headers): + sheet.write(0, i, header) + + for i, room in enumerate(Room.objects.all()): + row = i + 1 + sheet.write(row, 0, room.name) + sheet.write(row, 1, room.rank) + + return book + + +def export_team_stats_df(): + entries = [] + for team in Team.objects.all(): + entries.append({ + 'team_name': team.name, + 'team_wins': tab_logic.tot_wins(team), + 'team_speaks': tab_logic.tot_speaks(team), + 'team_ranks': tab_logic.tot_ranks(team), + 'team_speaks_single_adj': tab_logic.single_adjusted_speaks(team), + 'team_ranks_single_adj': tab_logic.single_adjusted_ranks(team), + 'team_speaks_double_adj': tab_logic.double_adjusted_speaks(team), + 'team_ranks_double_adj': tab_logic.double_adjusted_ranks(team), + 'team_opp_str': tab_logic.opp_strength(team) + }) + + return pd.DataFrame(entries) + + +def export_team_stats(): + """Exports data as XLS for each team: win-loss record, total speaker points, total ranks, single-adjusted + speaker points, single-adjusted ranks, double-adjusted speaker points, double-adjusted ranks, and opposition + strength.""" + book = Workbook('utf-8') + sheet = book.add_sheet('Team stats') + + sheet.write(0, 0, + 'If you are calculating the break off of these statistics, please pay attention to how you sort. ' + 'In general, everything except ranks should be sorted from large to small.') + + headers = ['team_name', 'team_wins', 'team_speaks', 'team_ranks', 'team_speaks_single_adj', + 'team_ranks_single_adj', 'team_speaks_double_adj', 'team_ranks_double_adj', 'team_opp_str'] + for i, header in enumerate(headers): + sheet.write(1, i, header) + + for i, team in enumerate(Team.objects.all()): + row = i + 2 + + sheet.write(row, 0, team.name) + sheet.write(row, 1, tab_logic.tot_wins(team)) + sheet.write(row, 2, tab_logic.tot_speaks(team)) + sheet.write(row, 3, tab_logic.tot_ranks(team)) + sheet.write(row, 4, tab_logic.single_adjusted_speaks(team)) + sheet.write(row, 5, tab_logic.single_adjusted_ranks(team)) + sheet.write(row, 6, tab_logic.double_adjusted_speaks(team)) + sheet.write(row, 7, tab_logic.double_adjusted_ranks(team)) + sheet.write(row, 8, tab_logic.opp_strength(team)) + + return book + + +def export_debater_stats_df(): + entries = [] + for debater in Debater.objects.all(): + debater_team = debater.team_set.first() + entries.append({ + 'debater_name': debater.name, + 'debater_speaks': tab_logic.tot_speaks_deb(debater, average_ironmen=True), + 'debater_ranks': tab_logic.tot_ranks_deb(debater, True), + 'debater_speaks_single_adj': tab_logic.single_adjusted_speaks_deb(debater), + 'debater_ranks_single_adj': tab_logic.single_adjusted_ranks_deb(debater), + 'debater_speaks_double_adj': tab_logic.double_adjusted_speaks_deb(debater), + 'debater_ranks_double_adj': tab_logic.double_adjusted_ranks_deb(debater), + 'debater_team_wins': tab_logic.tot_wins(debater_team), + 'debater_team_opp_strength': tab_logic.opp_strength(debater_team) + }) + + return pd.DataFrame(entries) + + +def export_debater_stats(): + """Exports data as XLS for each speaker: total speaker points, total ranks, single adjusted speaks, single adjusted + ranks, double adjusted speaks, double adjusted ranks, team performance, opposition strength. Automatically averages + for iron-men. """ + book = Workbook('utf-8') + sheet = book.add_sheet('Team stats') + + sheet.write(0, 0, + 'If you are calculating the awards off these statistics, please pay attention to how you sort. ' + 'In general, everything except ranks should be sorted from large to small.') + + headers = ['debater_name', 'debater_speaks', 'debater_ranks', 'debater_speaks_single_adj', + 'debater_ranks_single_adj', 'debater_speaks_double_adj', 'debater_ranks_double_adj', 'debater_team_wins', + 'debater_team_opp_strength'] + for i, header in enumerate(headers): + sheet.write(1, i, header) + + for i, debater in enumerate(Debater.objects.all()): + row = i + 2 + debater_team = debater.team_set.first() + + sheet.write(row, 0, debater.name) + sheet.write(row, 1, tab_logic.tot_speaks_deb(debater, average_ironmen=True)) + sheet.write(row, 2, tab_logic.tot_ranks_deb(debater, True)) + sheet.write(row, 3, tab_logic.single_adjusted_speaks_deb(debater)) + sheet.write(row, 4, tab_logic.single_adjusted_ranks_deb(debater)) + sheet.write(row, 5, tab_logic.double_adjusted_speaks_deb(debater)) + sheet.write(row, 6, tab_logic.double_adjusted_ranks_deb(debater)) + + # debater stats + sheet.write(row, 7, tab_logic.tot_wins(debater_team)) + sheet.write(row, 8, tab_logic.opp_strength(debater_team)) + + return book diff --git a/mittab/libs/data_import/import_judges.py b/mittab/libs/data_import/import_judges.py index 5a6ec8c22..3ced8654c 100644 --- a/mittab/libs/data_import/import_judges.py +++ b/mittab/libs/data_import/import_judges.py @@ -1,84 +1,93 @@ -#Copyright (C) 2011 by Julia Boortz and Joseph Lynch - -#Permission is hereby granted, free of charge, to any person obtaining a copy -#of this software and associated documentation files (the "Software"), to deal -#in the Software without restriction, including without limitation the rights -#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the Software is -#furnished to do so, subject to the following conditions: - -#The above copyright notice and this permission notice shall be included in -#all copies or substantial portions of the Software. - -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -#THE SOFTWARE. +# Copyright (C) 2011 by Julia Boortz and Joseph Lynch -from mittab.apps.tab.models import * -from mittab.apps.tab.forms import JudgeForm +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from __future__ import print_function from decimal import * import xlrd -from xlwt import Workbook -def import_judges(fileToImport): +from mittab.apps.tab.forms import JudgeForm +from mittab.apps.tab.models import * + + +def import_judges(import_file, using_overwrite=False): try: - sh = xlrd.open_workbook(filename=None, file_contents=fileToImport.read()).sheet_by_index(0) + sh = xlrd.open_workbook(filename=None, file_contents=import_file.read()).sheet_by_index(0) except: - return ["ERROR: Please upload an .xlsx file. This filetype is not compatible"] - num_judges = 0 + return ["ERROR: Please upload an .xls file. This file type is not compatible"] + + judge_entries = 0 found_end = False - judge_errors = [] - while found_end == False: + + errors = [] + while not found_end: try: - sh.cell(num_judges, 0).value - num_judges += 1 + sh.cell(judge_entries, 0).value # find file end + judge_entries += 1 except IndexError: found_end = True - #Verify sheet has required number of columns + # Verify sheet has required number of columns try: sh.cell(0, 1).value - except: - team_errors.append("ERROR: Insufficient Columns in sheet. No Data Read") - return team_errors - for i in range(1, num_judges): - #Load and validate Judge's Name + except IndexError: + errors.append("ERROR: Insufficient Columns in sheet. No Data Read") + return errors + + for i in range(1, judge_entries): + + # 0 name + # 1 rank + # 2+ schools + + is_duplicate = False + + # Load and validate Judge's Name judge_name = sh.cell(i, 0).value - try: - Judge.objects.get(name=judge_name) - judge_errors.append(judge_name + ": Duplicate Judge Name") - continue - except: - pass + if Judge.objects.filter(name=judge_name).exists(): + errors.append(judge_name + ': duplicate judge name, skipping') + is_duplicate = True - #Load and validate judge_rank + # Load and validate judge_rank judge_rank = sh.cell(i, 1).value try: - judge_rank = round(float(judge_rank), 2) - except: - judge_errors.append(judge_name + ": Rank not number") + judge_rank = Decimal(judge_rank) + except TypeError or ValueError: + errors.append(judge_name + ": Rank is not a number") continue + if judge_rank > 100 or judge_rank < 0: - judge_errors.append(judge_name + ": Rank should be between 0-100") + errors.append(judge_name + ": Rank should be between 0-100") continue - #iterate through schools until none are left + # iterate through schools until none are left cur_col = 2 schools = [] - while(True): + while True: try: judge_school = sh.cell(i, cur_col).value - #If other judges have more schools but this judge doesn't, we get an empty string - #If blank, keep iterating in case user has a random blank column for some reason - if (judge_school != ''): + # If other judges have more schools but this judge doesn't, we get an empty string + # If blank, keep iterating in case user has a random blank column for some reason + if judge_school != '': try: - #Get id from the name because JudgeForm requires we use id - s = School.objects.get(name__iexact=judge_school).id + # Get id from the name because JudgeForm requires we use id + s = School.objects.get(name=judge_school).id schools.append(s) except IndexError: break @@ -88,20 +97,30 @@ def import_judges(fileToImport): s.save() schools.append(s.id) except: - judge_errors.append(judge_name + ': Invalid School') + errors.append(judge_name + ': Invalid School') continue except IndexError: break cur_col += 1 - data = {'name': judge_name, 'rank': judge_rank, 'schools': schools} - form = JudgeForm(data=data) - if (form.is_valid()): - form.save() + if not is_duplicate: + form = JudgeForm(data={'name': judge_name, 'rank': judge_rank, 'schools': schools}) + if form.is_valid(): + form.save() + else: + print(form.errors) + errors.append(judge_name + ": Form invalid. Check inputs.") + else: - error_messages = sum([ error[1] for error in form.errors.items() ], []) - error_string = ', '.join(error_messages) - judge_errors.append("%s: %s" % (judge_name, error_string)) + if using_overwrite: + # overwrite the parameters for that judge if using overwrite + judge = Judge.objects.get(name=judge_name) + judge.rank = judge_rank + judge.school = schools + judge.save() - return judge_errors + else: + # do nothing + pass + return errors diff --git a/mittab/libs/data_import/import_rooms.py b/mittab/libs/data_import/import_rooms.py index 2d2d0c87d..4ae47434b 100644 --- a/mittab/libs/data_import/import_rooms.py +++ b/mittab/libs/data_import/import_rooms.py @@ -1,46 +1,46 @@ -#Copyright (C) 2011 by Julia Boortz and Joseph Lynch - -#Permission is hereby granted, free of charge, to any person obtaining a copy -#of this software and associated documentation files (the "Software"), to deal -#in the Software without restriction, including without limitation the rights -#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the Software is -#furnished to do so, subject to the following conditions: - -#The above copyright notice and this permission notice shall be included in -#all copies or substantial portions of the Software. - -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -#THE SOFTWARE. +# Copyright (C) 2011 by Julia Boortz and Joseph Lynch -from mittab.apps.tab.models import * -from mittab.apps.tab.forms import RoomForm +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. from decimal import * import xlrd -from xlwt import Workbook -def import_rooms(fileToImport): +from mittab.apps.tab.models import * + + +def import_rooms(import_file, using_overwrite=False): try: - sh = xlrd.open_workbook(filename=None, file_contents=fileToImport.read()).sheet_by_index(0) + sh = xlrd.open_workbook(filename=None, file_contents=import_file.read()).sheet_by_index(0) except: return ["ERROR: Please upload an .xlsx file. This filetype is not compatible"] num_rooms = 0 found_end = False room_errors = [] - while found_end == False: + + while not found_end: try: sh.cell(num_rooms, 0).value - num_rooms +=1 + num_rooms += 1 except IndexError: found_end = True - #Verify sheet has required number of columns + # Verify sheet has required number of columns try: sh.cell(0, 1).value except: @@ -48,37 +48,56 @@ def import_rooms(fileToImport): return room_errors for i in range(1, num_rooms): + + # headers are + # name, room rank + room_name = sh.cell(i, 0).value if room_name == '': - room_errors.append("Row " + str(i) + ": Empty Room Name") - continue - try: - Room.objects.get(name=room_name) - room_errors.append(room_name + ': Duplicate Room Name') + room_errors.append("Row " + str(i) + ": Empty room name") continue - except: - pass - #Load and validate room_rank + duplicate = False + if Room.objects.filter(name=room_name).exists(): # check for duplicates + room_errors.append(room_name + ': Duplicate room name') + duplicate = True + + # Load and validate room_rank room_rank = sh.cell(i, 1).value - room_string = str(room_rank) try: - room_rank = Decimal(room_rank) - except: - room_errors.append(room_name + ": Rank not number") + # auto-round to two floating point digits + room_rank = round(Decimal(room_rank), 2) + except TypeError or ValueError: + room_errors.append(room_name + ": Rank in file is not a number") continue - if len(room_string) > 5 or (room_rank < 10 and len(room_string) > 4): - room_errors.append(room_name + ": Rank should have no more than two decimal places") + + # cap room rank at 100 + if room_rank >= 100: + room_errors.append(room_name + ": is above 100, fix") continue - if room_rank >= 100 or room_rank < 0: - room_errors.append(room_name + ": Rank should be between 0-99.99") + + # floor room rank at 0 + if room_rank < 0: + room_errors.append(room_name + ": is below 0, fix") continue - #Create the room - room = Room(name=room_name, rank=room_rank); - try: + # save + if not duplicate: # Create the room + room = Room(name=room_name, rank=room_rank) room.save() - except: - room_errors.append(room_name + ": Unknown Error") + + else: # else, update room + if using_overwrite: + room = Room.objects.get(name=room_name) + room.rank = room_rank + + try: + room.save() + except: + room_errors.append(room_name + ": Error in saving") + + else: + # do nothing + pass return room_errors diff --git a/mittab/libs/data_import/import_scratches.py b/mittab/libs/data_import/import_scratches.py index 839318736..c48a41934 100644 --- a/mittab/libs/data_import/import_scratches.py +++ b/mittab/libs/data_import/import_scratches.py @@ -1,55 +1,100 @@ -#Copyright (C) 2011 by Julia Boortz and Joseph Lynch - -#Permission is hereby granted, free of charge, to any person obtaining a copy -#of this software and associated documentation files (the "Software"), to deal -#in the Software without restriction, including without limitation the rights -#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the Software is -#furnished to do so, subject to the following conditions: - -#The above copyright notice and this permission notice shall be included in -#all copies or substantial portions of the Software. - -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -#THE SOFTWARE. +# Copyright (C) 2011 by Julia Boortz and Joseph Lynch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import print_function +import pandas as pd +from django.db import IntegrityError + +from xlrd import XLRDError from mittab.apps.tab.models import * -import xlrd -import csv -from xlwt import Workbook - -def import_scratches(fileToImport): - sh = xlrd.open_workbook(fileToImport).sheet_by_index(0) - num_scratches = 0 - found_end = False + + +def _verify_scratch_type(s_type): + # TYPE_CHOICES = ( + # (TEAM_SCRATCH, u'Team Scratch'), + # (TAB_SCRATCH, u'Tab Scratch'), + # ) + try: + # if input is substring at start of lower-cased description (i.e. 'team'), match + return next(i for i, description in Scratch.TYPE_CHOICES if description.lower().startswith(s_type.lower())) + except AttributeError: + # attribute error raised if type(s_type) != str + # check whether inputs contained in type choices' integer codes + return next(i for i, description in Scratch.TYPE_CHOICES if i == s_type) + + +def import_scratches(import_file): + try: # try to read as excel + scratch_df = pd.read_excel(import_file) + except XLRDError: # if not excel, try as CSV + scratch_df = pd.read_csv(import_file) + scratch_errors = [] - while found_end == False: - try: - sh.cell(num_scratches,0).value - num_scratches +=1 - except IndexError: - found_end = True - for i in range(1, num_scratches): + required_columns = {'team_name', 'judge_name', 'scratch_type'} + + if set(scratch_df.columns.values.tolist()) != required_columns: + # checks whether the required columns are contained, if false, then... + scratch_errors.append('missing columns, needed columns are {}'.format(required_columns)) + return scratch_errors + + for i, row in scratch_df.iterrows(): + team_name = row['team_name'] + judge_name = row['judge_name'] + scratch_type = row['scratch_type'] + + try: # clean the scratch type + clean_stype = _verify_scratch_type(scratch_type) + except StopIteration: + scratch_errors.append('error on line {}, scratch type {} is not valid code, skip'.format(i, scratch_type)) + continue + + except Exception as e: + scratch_errors.append('error on line {} unknown non-conforming problem, skip'.format(i, scratch_type)) + print(e) + continue + try: - team_name = sh.cell(i,0).value - t = Team.objects.get(name = team_name) - judge_name = sh.cell(i,1).value - j = Judge.objects.get(name = judge_name) - s_type = sh.cell(i,2).value.lower() - if s_type == "team scratch" or s_type == "team": - s_type = 0 - elif s_type == "tab scratch" or s_type == "tab": - s_type = 1 - s = Scratch(judge = j, team = t, scratch_type = s_type) - try: - s.save() - except: - scratch_errors.append[[j,t]] + team = Team.objects.get(name=team_name) + judge = Judge.objects.get(name=judge_name) + scratch = Scratch(team=team, judge=judge, scratch_type=clean_stype) + scratch.save() + print('saved scratch on {} by {}'.format(judge.name, team.name)) + + except Team.DoesNotExist: + scratch_errors.append('could not find team with name {}. skipped'.format(team_name)) + continue + + except Judge.DoesNotExist: + scratch_errors.append('could not find judge with name {}. skipped'.format(judge_name)) + continue + + except IntegrityError: + # duplicated? skip + scratch_errors.append('could not save scratch on {} by team {},' + ' exists. skipped'.format(judge_name, team_name)) + continue + except Exception as e: - print e + scratch_errors.append('could not save scratch on {} by team {}, unknown err'.format(judge_name, team_name)) + print(e) + continue + + # return errors return scratch_errors diff --git a/mittab/libs/data_import/import_teams.py b/mittab/libs/data_import/import_teams.py index cfd40398c..6fb18d16e 100644 --- a/mittab/libs/data_import/import_teams.py +++ b/mittab/libs/data_import/import_teams.py @@ -1,69 +1,118 @@ -#Copyright (C) 2011 by Julia Boortz and Joseph Lynch - -#Permission is hereby granted, free of charge, to any person obtaining a copy -#of this software and associated documentation files (the "Software"), to deal -#in the Software without restriction, including without limitation the rights -#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -#copies of the Software, and to permit persons to whom the Software is -#furnished to do so, subject to the following conditions: - -#The above copyright notice and this permission notice shall be included in -#all copies or substantial portions of the Software. - -#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -#THE SOFTWARE. +# Copyright (C) 2011 by Julia Boortz and Joseph Lynch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import collections +import xlrd +from django.core.exceptions import ObjectDoesNotExist -from mittab.apps.tab.models import * from mittab.apps.tab.forms import SchoolForm +from mittab.apps.tab.models import * + + +def _create_status(status): + """Translates the string for varsity-novice status into MIT-TAB's integer pseudo-enum""" + if status == 'novice' or status == 'nov' or status == 'n': + return Debater.NOVICE + else: + return Debater.VARSITY -import xlrd -from xlwt import Workbook -def import_teams(fileToImport): +def _translate_seed(team_name, seed): + """Translates the string version of the seed into the pseudo-enum. Checks for duplicate free seeds and changes it + as necessary. Also notes that change so a message can be returned. + :type team_name: str + :type seed: str + :return seed integer code + """ + seed_int = Team.FREE_SEED + seed_changed = False + + if seed == 'full seed' or seed == 'full': + seed_int = Team.FULL_SEED + elif seed == 'half seed' or seed == 'half': + seed_int = Team.HALF_SEED + elif seed == 'free seed' or seed == 'free': + seed_int = Team.FREE_SEED + + return seed_int + + +def import_teams(import_file, using_overwrite=False): try: - sh = xlrd.open_workbook(filename=None, file_contents=fileToImport.read()).sheet_by_index(0) + sh = xlrd.open_workbook(filename=None, file_contents=import_file.read()).sheet_by_index(0) except: return ['ERROR: Please upload an .xlsx file. This filetype is not compatible'] + num_teams = 0 found_end = False team_errors = [] while found_end == False: try: sh.cell(num_teams, 0).value - num_teams +=1 + num_teams += 1 except IndexError: found_end = True - #Verify sheet has required number of columns + # Verify sheet has required number of columns try: sh.cell(0, 5).value except: team_errors.append('ERROR: Insufficient Columns in Sheet. No Data Read') return team_errors + # verify no duplicate debaters, give error messages + deb_indicies = [] for i in range(1, num_teams): + deb_indicies.append((sh.cell(i, 3).value.strip(), i)) # tuple saves debater name and row + deb_indicies.append((sh.cell(i, 7).value.strip(), i)) + + deb_names = [i[0] for i in deb_indicies] + names_dict = collections.Counter(deb_names) + for deb_index in deb_indicies: + if names_dict.get(deb_index[0]) > 1: # if dict says appears more than once + # inform that duplicate exists at location, report relevant information + row_num = deb_index[1] + msg = "Check for duplicate debater " + deb_index[0] + " in team " + sh.cell(row_num, 0).value + \ + ", on XLS file row " + str(row_num) + team_errors.append(msg) + for i in range(1, num_teams): + + # Name, School, Seed [full, half, free, none], D1 name, D1 v/n? + # D2 name, D2 v/n? + + # team name, check for duplicates + duplicate = False team_name = sh.cell(i, 0).value if team_name == '': - team_errors.append('Row ' + str(i) + ': Empty Team Name') + team_errors.append('Skipped row ' + str(i) + ': empty Team Name') continue - try: - Team.objects.get(name=team_name) - team_errors.append(team_name + ': Duplicate Team Name') - continue - except: - pass + if Team.objects.filter(name=team_name).exists(): # inform that duplicates exist + duplicate = True + team_errors.append(team_name + ': duplicate team, overwriting data') school_name = sh.cell(i, 1).value.strip() try: team_school = School.objects.get(name__iexact=school_name) except: - #Create school through SchoolForm because for some reason they don't save otherwise + # Create school through SchoolForm because for some reason they don't save otherwise form = SchoolForm(data={'name': school_name}) if form.is_valid(): form.save() @@ -72,101 +121,50 @@ def import_teams(fileToImport): continue team_school = School.objects.get(name__iexact=school_name) - hybrid_school_name = sh.cell(i, 2).value.strip() - hybrid_school = None - if hybrid_school_name != '': - try: - hybrid_school = School.objects.get(name__iexact=hybrid_school_name) - except: - #Create school through SchoolForm because for some reason they don't save otherwise - form = SchoolForm(data={'name': hybrid_school_name}) - if form.is_valid(): - form.save() - hybrid_school = School.objects.get(name__iexact=hybrid_school_name) - else: - team_errors.append(team_name + ": Invalid Hybrid School") - continue - - team_seed = sh.cell(i, 3).value.strip().lower() - if team_seed == 'full seed' or team_seed == 'full': - team_seed = 3 - elif team_seed == 'half seed' or team_seed == 'half': - team_seed = 2 - elif team_seed == 'free seed' or team_seed == 'free': - team_seed = 1 - elif team_seed == 'unseeded' or team_seed == 'un' or team_seed == 'none' or team_seed == '': - team_seed = 0 - else: - team_errors.append(team_name + ': Invalid Seed Value') - continue - - deb1_name = sh.cell(i, 4).value - if deb1_name == '': - team_errors.append(team_name + ': Empty Debater-1 Name') - continue - try: - Debater.objects.get(name=deb1_name) - team_errors.append(team_name + ': Duplicate Debater-1 Name') - continue - except: - pass - deb1_status = sh.cell(i, 5).value.lower() - if deb1_status == 'novice' or deb1_status == 'nov' or deb1_status == 'n': - deb1_status = 1 - else: - deb1_status = 0 - - iron_man = False - deb2_name = sh.cell(i, 6).value - - if deb2_name == '': - iron_man = True - if (not iron_man): - try: - Debater.objects.get(name=deb2_name) - team_errors.append(team_name + ': Duplicate Debater-2 Name') - continue - except: - pass - deb2_status = sh.cell(i, 7).value.lower() - - if deb2_status == 'novice' or deb2_status == 'nov' or deb2_status == 'n': - deb2_status = 1 - else: - deb2_status = 0 - - #Save Everything - try: - deb1 = Debater(name=deb1_name, novice_status=deb1_status) - deb1.save() - except: - team_errors.append(team_name + ': Unkown Error Saving Debater 1') - continue - if (not iron_man): - try: - deb2 = Debater(name=deb2_name, novice_status=deb2_status) - deb2.save() - except: - team_errors.append(team_name + ': Unkown Error Saving Debater 2') - team_errors.append(' WARNING: Debaters on this team may be added to database. ' + - 'Please Check this Manually') - continue - - team = Team(name=team_name, school=team_school, - hybrid_school=hybrid_school, seed=team_seed) - - try: + # check seeds, do check for multiple free seeds and report + team_seed = _translate_seed(team_name=team_name, seed=sh.cell(i, 2).value.strip().lower()) + school = Team.objects.get(name=team_name).school # get school_name + if any(int(team.seed) == Team.FREE_SEED for team in school.team_set): # multiple free seeds exist + team_errors.append('school ' + school.name + ' has more than one free seed. confirm this.') + + # todo something about hybrid schools? + deb1_name = sh.cell(i, 3).value.strip() + deb1_status = _create_status(sh.cell(i, 4).value.lower()) + deb1, deb1_created = Debater.objects.get_or_create(name=deb1_name, novice_status=deb1_status) + + iron_man = True + deb2_name = sh.cell(i, 5).value.strip() + if deb2_name is not '': + iron_man = False + deb2_status = _create_status(sh.cell(i, 6).value.lower()) + deb2, deb2_created = Debater.objects.get_or_create(name=deb2_name, novice_status=deb2_status) + + if not duplicate: # create new team + team = Team(name=team_name, school=team_school, seed=team_seed) team.save() team.debaters.add(deb1) - if (not iron_man): + if not iron_man: team.debaters.add(deb2) else: - team_errors.append(team_name + ": Detected to be Iron Man - Still added successfully") + team_errors.append(team_name + ': Team is an iron-man, added successfully') + team.save() - except: - team_errors.append(team_name + ': Unknown Error Saving Team') - team_errors.append(' WARNING: Debaters on this team may be added to database. ' + - 'Please Check this Manually') - return team_errors + else: # update the team + if using_overwrite: + team = Team.objects.get(name=team_name) + team.school = team_school + team.seed = team_seed + team.debaters.clear() + team.debaters.add(deb1) + if not iron_man: + team.debaters.add(deb2) + else: + team_errors.append(team_name + ': Team is an iron-man, added successfully') + team.save() + else: + # not overwriting, do nothing + pass + + return team_errors diff --git a/mittab/libs/structs.py b/mittab/libs/structs.py new file mode 100644 index 000000000..8c4c8cdeb --- /dev/null +++ b/mittab/libs/structs.py @@ -0,0 +1,56 @@ +class DebaterScores(object): + def __init__(self, s, t): + # (-tot_speaks_deb(debater), + # tot_ranks_deb(debater), + # -single_adjusted_speaks_deb(debater), + # single_adjusted_ranks_deb(debater), + # -double_adjusted_speaks_deb(debater), + # double_adjusted_ranks_deb(debater)) + self.speaker = s + self.tot_speaks = -t[0] + self.tot_ranks = t[1] + self.s_adj_speaks = -t[2] + self.s_adj_ranks = t[3] + self.d_adj_speaks = -t[4] + self.d_adj_ranks = t[5] + + def create_scoring_tuple(self): + return ( + -self.tot_speaks, + self.tot_ranks, + -self.s_adj_speaks, + self.s_adj_ranks, + -self.d_adj_speaks, + self.d_adj_ranks + ) + + +class TeamScores(object): + def __init__(self, team, data): + # (-tot_wins(team), + # -tot_speaks(team), + # tot_ranks(team), + # -single_adjusted_speaks(team), + # single_adjusted_ranks(team), + # -double_adjusted_speaks(team), + # double_adjusted_ranks(team), + # -opp_strength(team)) + self.team = team + self.wins = -data[0] + self.tot_speaks = -data[1] + self.tot_ranks = data[2] + self.s_adj_speaks = -data[3] + self.s_adj_ranks = data[4] + self.d_adj_speaks = -data[5] + self.d_adj_ranks = data[6] + + def create_scoring_tuple(self): + return ( + -self.wins, + -self.tot_speaks, + self.tot_ranks, + -self.s_adj_speaks, + self.s_adj_ranks, + -self.d_adj_speaks, + self.d_adj_ranks + ) diff --git a/mittab/libs/tab_logic.py b/mittab/libs/tab_logic.py index f9e48b941..c019dc4fa 100644 --- a/mittab/libs/tab_logic.py +++ b/mittab/libs/tab_logic.py @@ -13,7 +13,7 @@ import itertools from cache_logic import cache - +from mittab.libs.structs import DebaterScores, TeamScores MAXIMUM_DEBATER_RANKS = 3.5 MINIMUM_DEBATER_SPEAKS = 0.0 @@ -350,11 +350,14 @@ def pull_up_count(t): pullups += 1 return pullups + def num_opps(t): - return Round.objects.filter(opp_team=t).count() + return t.opp_team.count() + def num_govs(t): - return Round.objects.filter(gov_team=t).count() + return t.gov_team.count() + def had_bye(t): return Bye.objects.filter(bye_team=t).exists() @@ -425,56 +428,75 @@ def team_wins_by_forfeit(): wins_by_forfeit.append(r.opp_team) return list(set(wins_by_forfeit)) + # Calculate the total number of wins a team has def tot_wins(team): - tot_wins = Round.objects.filter( - Q(gov_team=team, victor=Round.GOV)| - Q(opp_team=team, victor=Round.OPP)).count() - # If a team had the bye, they won't have a round for that win so add one win - tot_wins += num_byes(team) - # If a team has won by forfeit, we didn't count that yet - tot_wins += num_forfeit_wins(team) - return tot_wins + gov_wins = team.gov_team.filter( + Q(victor=Round.GOV) | # gov win + Q(victor=Round.GOV_VIA_FORFEIT) | # gov via forfeit + Q(victor=Round.ALL_WIN) # gov all win + ).count() + + opp_wins = team.opp_team.filter( + Q(victor=Round.OPP) | # opp win + Q(victor=Round.OPP_VIA_FORFEIT) | # opp via forfeit + Q(victor=Round.ALL_WIN) # opp all win + ).count() + + win_count = gov_wins + opp_wins + win_count += num_byes(team) + return win_count + + +def _single_adjust(sorted_list): + """ NOTE: Does not run a sort """ + return sorted_list[1: -1] + + +def _double_adjust(sorted_list): + """ NOTE: Does not run a sort """ + return sorted_list[2: -2] + """ Speaks """ @cache() +def _team_speaks_list(team): + speaks = [speaks_for_debater(deb, False) for deb in team.debaters.all()] + return sorted([item for sublist in speaks for item in sublist]) + + def tot_speaks(team): - tot_speaks = sum([tot_speaks_deb(deb, False) - for deb in team.debaters.all()]) - return tot_speaks + return sum(_team_speaks_list(team)) + -@cache() def single_adjusted_speaks(team): - speaks = [speaks_for_debater(deb, False) for deb in team.debaters.all()] - speaks = sorted([item for sublist in speaks for item in sublist]) - return sum(speaks[1:-1]) + """ Lays out all the speaks gained by members of the team. Sorts them, then removes the lowest and highest. """ + return sum(_single_adjust(_team_speaks_list(team))) + -@cache() def double_adjusted_speaks(team): - speaks = [speaks_for_debater(deb, False) for deb in team.debaters.all()] - speaks = sorted([item for sublist in speaks for item in sublist]) - return sum(speaks[2:-2]) + return sum(_double_adjust(_team_speaks_list(team))) + """ Ranks """ @cache() +def _team_ranks_list(t): + ranks = [ranks_for_debater(deb, False) for deb in t.debaters.all()] + return sorted([item for sublist in ranks for item in sublist]) + + def tot_ranks(team): - tot_ranks = sum([tot_ranks_deb(deb, False) - for deb in team.debaters.all()]) - return tot_ranks + return sum(_team_ranks_list(team)) + -@cache() def single_adjusted_ranks(team): - ranks = [ranks_for_debater(deb, False) for deb in team.debaters.all()] - ranks = sorted([item for sublist in ranks for item in sublist]) - return sum(ranks[1:-1]) + return sum(_single_adjust(_team_ranks_list(team))) + -@cache() def double_adjusted_ranks(team): - ranks = [ranks_for_debater(deb, False) for deb in team.debaters.all()] - ranks = sorted([item for sublist in ranks for item in sublist]) - return sum(ranks[2:-2]) + return sum(_double_adjust(_team_ranks_list(team))) + -@cache() def opp_strength(t): """ Average number of wins per opponent @@ -499,7 +521,7 @@ def opp_strength(t): else: return 0.0 -# Return a list of all teams who have no varsity members +# Return a list of all teams who have no varsity members def all_nov_teams(): return list(Team.objects.exclude(debaters__novice_status__exact=Debater.VARSITY)) @@ -507,6 +529,7 @@ def all_nov_teams(): def all_teams(): return list(Team.objects.all()) + def team_comp(pairing, round_number): gov, opp = pairing.gov_team, pairing.opp_team if round_number == 1: @@ -517,25 +540,40 @@ def team_comp(pairing, round_number): max(tot_speaks(gov), tot_speaks(opp)), min(tot_speaks(gov), tot_speaks(opp))) + def team_score(team): """A tuple representing the passed team's performance at the tournament""" - score = (0,0,0,0,0,0,0,0) + score = (0, 0, 0, 0, 0, 0, 0, 0) try: + team_speaks = _team_speaks_list(team) + team_ranks = _team_ranks_list(team) score = (-tot_wins(team), - -tot_speaks(team), - tot_ranks(team), - -single_adjusted_speaks(team), - single_adjusted_ranks(team), - -double_adjusted_speaks(team), - double_adjusted_ranks(team), + -sum(team_speaks), + sum(team_ranks), + -sum(_single_adjust(team_speaks)), + sum(_single_adjust(team_ranks)), + -sum(_double_adjust(team_speaks)), + sum(_double_adjust(team_ranks)), -opp_strength(team)) + except Exception: errors.emit_current_exception() + + print('scored team {}'.format(team.name)) return score + def team_score_except_record(team): return team_score(team)[1:] + +def get_team_scores(): + return [TeamScores(t, team_score(t)) for t in + Team.objects.prefetch_related('debaters', 'debaters__roundstats_set', + 'debaters__roundstats_set', 'debaters__roundstats_set__round', + 'debaters__team_set__bye_set', 'debaters__team_set__noshow_set').all()] + + def rank_teams(): return sorted(all_teams(), key=team_score) @@ -571,7 +609,7 @@ def avg_deb_speaks(debater): for roundstat in debater_roundstats: speaks_per_round[roundstat.round.round_number].append(roundstat) - for round_number in range(1, num_speaks + 1): + for round_number in xrange(1, num_speaks + 1): roundstats = speaks_per_round[round_number] if roundstats: speaks = [float(rs.speaks) for rs in roundstats] @@ -594,7 +632,6 @@ def debater_forfeit_speaks(debater): Note that right now we just return 0, but we may want to add support for returning average speaks or some such """ - return 0.0 @cache() @@ -631,9 +668,9 @@ def speaks_for_debater(debater, average_ironmen=True): for roundstat in debater_roundstats: speaks_per_round[roundstat.round.round_number].append(roundstat) - for round_number in range(1, num_speaks + 1): + for round_number in xrange(1, num_speaks + 1): roundstats = speaks_per_round[round_number] - if roundstats: + if len(roundstats) > 0: # This is so if in the odd chance we get a debater paired in # twice we take the speaks they actually got roundstats.sort(key=lambda rs: rs.speaks, reverse=True) @@ -643,15 +680,19 @@ def speaks_for_debater(debater, average_ironmen=True): speaks = [float(rs.speaks) for rs in roundstats] avg_speaks = sum(speaks) / float(len(roundstats)) + if won_by_forfeit(roundstat.round, team): debater_speaks.append(avg_deb_speaks(debater)) + elif forfeited_round(roundstat.round, team): debater_speaks.append(MINIMUM_DEBATER_SPEAKS) + else: if average_ironmen: debater_speaks.append(avg_speaks) else: debater_speaks.extend(speaks) + else: speaks = debater_abnormal_round_speaks(debater, round_number) if speaks is not None: @@ -681,13 +722,12 @@ def debater_abnormal_round_speaks(debater, round_number): def single_adjusted_speaks_deb(debater): debater_speaks = speaks_for_debater(debater) - debater_speaks.sort() - return sum(debater_speaks[1:-1]) + return sum(_single_adjust(sorted(debater_speaks))) + def double_adjusted_speaks_deb(debater): debater_speaks = speaks_for_debater(debater) - debater_speaks.sort() - return sum(debater_speaks[2:-2]) + return sum(_double_adjust(sorted(debater_speaks))) @cache() def tot_speaks_deb(debater, average_ironmen=True): @@ -711,7 +751,7 @@ def avg_deb_ranks(debater): forfeits to count as average ranks. """ real_ranks = [] - num_ranks = TabSettings.objects.get(key = 'cur_round').value - 1 + num_ranks = TabSettings.objects.get(key='cur_round').value - 1 debater_roundstats = debater.roundstats_set.all() team = deb_team(debater) @@ -721,15 +761,15 @@ def avg_deb_ranks(debater): for roundstat in debater_roundstats: ranks_per_round[roundstat.round.round_number].append(roundstat) - for round_number in range(1, num_ranks + 1): + for round_number in xrange(1, num_ranks + 1): roundstats = ranks_per_round[round_number] if roundstats: ranks = [float(rs.ranks) for rs in roundstats] avg_ranks = sum(ranks) / float(len(roundstats)) roundstat = roundstats[0] - if (won_by_forfeit(roundstat.round, team) or - forfeited_round(roundstat.round, team)): + if won_by_forfeit(roundstat.round, team) or forfeited_round(roundstat.round, team): continue + real_ranks.append(avg_ranks) if len(real_ranks) == 0: @@ -771,16 +811,20 @@ def ranks_for_debater(debater, average_ironmen=True): for roundstat in debater_roundstats: ranks_per_round[roundstat.round.round_number].append(roundstat) - for round_number in range(1, num_ranks + 1): + for round_number in xrange(1, num_ranks + 1): roundstats = ranks_per_round[round_number] - if roundstats: + if len(roundstats) > 0: ranks = [float(rs.ranks) for rs in roundstats] avg_ranks = sum(ranks) / float(len(roundstats)) + + # check first round roundstat = roundstats[0] if won_by_forfeit(roundstat.round, team): debater_ranks.append(avg_deb_ranks(debater)) + elif forfeited_round(roundstat.round, team): debater_ranks.append(MAXIMUM_DEBATER_RANKS) + else: if average_ironmen: debater_ranks.append(avg_ranks) @@ -794,6 +838,7 @@ def ranks_for_debater(debater, average_ironmen=True): debater_ranks = map(float, debater_ranks) return debater_ranks + def debater_abnormal_round_ranks(debater, round_number): """ Calculate the ranks for a bye/forfeit round @@ -806,53 +851,67 @@ def debater_abnormal_round_ranks(debater, round_number): Uses average ranks """ team = deb_team(debater) - had_bye = Bye.objects.filter(round_number=round_number, - bye_team=team) - had_noshow = NoShow.objects.filter(round_number=round_number, - no_show_team=team) + had_bye = Bye.objects.filter(round_number=round_number, bye_team=team) + had_noshow = NoShow.objects.filter(round_number=round_number, no_show_team=team) + if had_bye or (had_noshow and had_noshow.first().lenient_late): return avg_deb_ranks(debater) + elif had_noshow: return MAXIMUM_DEBATER_RANKS def single_adjusted_ranks_deb(debater): debater_ranks = ranks_for_debater(debater) - debater_ranks.sort() - return sum(debater_ranks[1:-1]) + return sum(_single_adjust(sorted(debater_ranks))) def double_adjusted_ranks_deb(debater): debater_ranks = ranks_for_debater(debater) - debater_ranks.sort() - return sum(debater_ranks[2:-2]) + return sum(_double_adjust(sorted(debater_ranks))) @cache() def tot_ranks_deb(debater, average_ironmen=True): debater_ranks = ranks_for_debater(debater, average_ironmen=average_ironmen) return sum(debater_ranks) + def deb_team(d): try: - return d.team_set.all()[0] - except: + return d.team_set.first() + except Team.DoesNotExist: return None -# Returns a tuple used for comparing two debaters + +# Returns a tuple used for comparing two debaters # in terms of their overall standing in the tournament def debater_score(debater): - score = (0,0,0,0,0,0) + score = (0, 0, 0, 0, 0, 0) try: - score = (-tot_speaks_deb(debater), - tot_ranks_deb(debater), - -single_adjusted_speaks_deb(debater), - single_adjusted_ranks_deb(debater), - -double_adjusted_speaks_deb(debater), - double_adjusted_ranks_deb(debater)) + speaks_list = sorted(speaks_for_debater(debater)) + ranks_list = sorted(ranks_for_debater(debater)) + score = ( + -sum(speaks_list), + sum(ranks_list), + -sum(_single_adjust(speaks_list)), + sum(_single_adjust(ranks_list)), + -sum(_double_adjust(speaks_list)), + sum(_double_adjust(ranks_list)) + ) + except Exception: errors.emit_current_exception() + print "finished scoring {}".format(debater) return score + +def get_debater_scores(): + return [DebaterScores(d, debater_score(d)) for d in + Debater.objects.prefetch_related('roundstats_set', 'roundstats_set__round', 'team_set', + 'team_set__bye_set', 'team_set__noshow_set').all()] + # prefetch roundstats to save on ORM time + + def rank_speakers(): return sorted(Debater.objects.all(), key=debater_score) diff --git a/mittab/settings.py b/mittab/settings.py index 066cadfe8..f97c8f7e4 100644 --- a/mittab/settings.py +++ b/mittab/settings.py @@ -21,6 +21,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get('DEBUG') +DEBUG = True ALLOWED_HOSTS = ['*'] diff --git a/mittab/urls.py b/mittab/urls.py index 7bf93b181..d11ee53be 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -7,8 +7,11 @@ import apps.tab.team_views as team_views import apps.tab.debater_views as debater_views import apps.tab.pairing_views as pairing_views +from mittab.apps.tab import export_views +from mittab.libs.data_import import export_xls_files from django.contrib import admin + admin.autodiscover() urlpatterns = [ @@ -100,13 +103,18 @@ url(r'^e_ballots/$', pairing_views.e_ballot_search), url(r'e_ballots/(\S+)/$', pairing_views.enter_e_ballot), - # Backups url(r'^backup/restore/(.+)/$', pairing_views.restore_backup), url(r'^backup/download/(.+)/$', pairing_views.download_backup), url(r'^backup/(.+)/$', pairing_views.view_backup), url(r'^upload_backup/$', pairing_views.upload_backup), - # Data Upload - url(r'^import_data/$', views.upload_data) + # Data upload and download + url(r'^import_data/$', views.upload_data, name='upload-data'), + url(r'^export/$', export_views.export_xls_portal, name='xls-export'), + url(r'^export/teams', export_views.export_team_xls, name='xls-teams'), + url(r'^export/judges', export_views.export_judge_xls, name='xls-judges'), + url(r'^export/rooms', export_views.export_room_xls, name='xls-rooms'), + url(r'^export/team-stats', export_views.export_team_stats_xls, name='xls-team-stats'), + url(r'^export/speaker-stats', export_views.export_debater_stats_xls, name='xls-debater-stats') ] diff --git a/requirements.txt b/requirements.txt index dad5f2233..52f33afdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,8 @@ numpy==1.12.1 pytest==2.4.2 django-localflavor==1.0 gunicorn==19.7.1 -xlrd==0.9.4 +xlrd>=1.0.0 xlwt==1.0.0 raven==6.1.0 unicodecsv==0.14.1 +pandas==0.24.1