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 %}
{{ 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