From 12a4e1b0483807759d6f51a5144d29e1437dc364 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 15:37:23 -0700 Subject: [PATCH 01/13] Start work on #461: remove default callable --- tabbycat/participants/emoji.py | 24 ++++++++++++++++++++++-- tabbycat/participants/models.py | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tabbycat/participants/emoji.py b/tabbycat/participants/emoji.py index 5909c036377..8ef4662b2d8 100644 --- a/tabbycat/participants/emoji.py +++ b/tabbycat/participants/emoji.py @@ -5,7 +5,25 @@ logger = logging.getLogger(__name__) -def pick_unused_emoji(teams=None, used=None): +def set_emoji(teams, tournament): + """Sets the emoji of every team in `teams` to a randomly chosen and unique + emoji. Every team in `teams` must be from the same tournament, and that + tournament must be provided as the second argument.""" + + used_emoji = tournament.team_set.filter(emoji__isnull=False).values_list('emoji', flat=True) + unused_emoji = [e[0] for e in EMOJI_LIST if e[0] not in used_emoji] + + if len(teams) > len(unused_emoji): + teams = teams[:len(unused_emoji)] + emojis = random.sample(unused_emoji, len(teams)) + + for team, emoji in zip(teams, emojis): + print(team, ord(emoji)) + team.emoji = emoji + team.save() + + +def pick_unused_emoji(teams=None, used=None, tournament=None): """Picks an emoji that is not already in use by any team in `teams`. If `teams` is not specified, it picks an emoji not in use by any team in the database. If no emoji are left, it returns `None`. @@ -13,7 +31,9 @@ def pick_unused_emoji(teams=None, used=None): If `used` is specified, it should be a list of emoji, and it also avoids emoji in `used` and appends the chosen emoji to the list. """ - if teams is None: + if teams is None and tournament is not None: + teams = tournament.team_set.all() + elif teams is None: from .models import Team teams = Team.objects.all() diff --git a/tabbycat/participants/models.py b/tabbycat/participants/models.py index 4530c3dee8a..ec479b87665 100644 --- a/tabbycat/participants/models.py +++ b/tabbycat/participants/models.py @@ -127,7 +127,7 @@ class Team(models.Model): (TYPE_BYE, 'Bye'), ) type = models.CharField(max_length=1, choices=TYPE_CHOICES, default=TYPE_NONE) - emoji = models.CharField(max_length=2, blank=True, null=True, default=pick_unused_emoji, choices=EMOJI_LIST) # uses null=True to allow multiple teams to have no emoji + emoji = models.CharField(max_length=2, blank=True, null=True, default=None, choices=EMOJI_LIST) # uses null=True to allow multiple teams to have no emoji construct_emoji = pick_unused_emoji # historical reference for migration 0026_auto_20170416_2332 From 92d9b24ff560ed7bb340074ef8485f7186181e90 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 15:45:55 -0700 Subject: [PATCH 02/13] Customize admin emoji field #461 --- tabbycat/participants/admin.py | 11 +++++++++-- tabbycat/participants/emoji.py | 6 ++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tabbycat/participants/admin.py b/tabbycat/participants/admin.py index 8bb02d4cf09..f3a153abe21 100644 --- a/tabbycat/participants/admin.py +++ b/tabbycat/participants/admin.py @@ -1,3 +1,4 @@ +import random from django.contrib import admin from django import forms @@ -6,6 +7,7 @@ from adjfeedback.models import AdjudicatorTestScoreHistory from venues.admin import VenueConstraintInline +from .emoji import pick_unused_emoji from .models import Adjudicator, Institution, Region, Speaker, Team @@ -66,8 +68,8 @@ class Meta: fields = '__all__' def clean_url_key(self): - return self.cleaned_data[ - 'url_key'] or None # So that the url key can be unique and be blank + # So that the url key can be unique and be blank + return self.cleaned_data['url_key'] or None class TeamAdmin(admin.ModelAdmin): @@ -84,6 +86,11 @@ def get_queryset(self, request): return super(TeamAdmin, self).get_queryset(request).prefetch_related( 'institution', 'division') + def formfield_for_choice_field(self, db_field, request, **kwargs): + print("formfield_for_choice_field,", db_field.name) + if db_field.name == 'emoji' and kwargs.get("initial", None) is None: + kwargs["initial"] = pick_unused_emoji() + return super().formfield_for_choice_field(db_field, request, **kwargs) admin.site.register(Team, TeamAdmin) diff --git a/tabbycat/participants/emoji.py b/tabbycat/participants/emoji.py index 8ef4662b2d8..48bda56c39e 100644 --- a/tabbycat/participants/emoji.py +++ b/tabbycat/participants/emoji.py @@ -23,7 +23,7 @@ def set_emoji(teams, tournament): team.save() -def pick_unused_emoji(teams=None, used=None, tournament=None): +def pick_unused_emoji(teams=None, used=None): """Picks an emoji that is not already in use by any team in `teams`. If `teams` is not specified, it picks an emoji not in use by any team in the database. If no emoji are left, it returns `None`. @@ -31,9 +31,7 @@ def pick_unused_emoji(teams=None, used=None, tournament=None): If `used` is specified, it should be a list of emoji, and it also avoids emoji in `used` and appends the chosen emoji to the list. """ - if teams is None and tournament is not None: - teams = tournament.team_set.all() - elif teams is None: + if teams is None: from .models import Team teams = Team.objects.all() From eb136ac80bf40fbb3db1e09523ec4d22e1e90f70 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 15:47:50 -0700 Subject: [PATCH 03/13] Update generateemoji management command #461 --- tabbycat/participants/admin.py | 1 - tabbycat/participants/emoji.py | 1 - tabbycat/participants/management/commands/generateemoji.py | 6 ++---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tabbycat/participants/admin.py b/tabbycat/participants/admin.py index f3a153abe21..13006c5c6ba 100644 --- a/tabbycat/participants/admin.py +++ b/tabbycat/participants/admin.py @@ -87,7 +87,6 @@ def get_queryset(self, request): 'institution', 'division') def formfield_for_choice_field(self, db_field, request, **kwargs): - print("formfield_for_choice_field,", db_field.name) if db_field.name == 'emoji' and kwargs.get("initial", None) is None: kwargs["initial"] = pick_unused_emoji() return super().formfield_for_choice_field(db_field, request, **kwargs) diff --git a/tabbycat/participants/emoji.py b/tabbycat/participants/emoji.py index 48bda56c39e..02a4c62a1e7 100644 --- a/tabbycat/participants/emoji.py +++ b/tabbycat/participants/emoji.py @@ -18,7 +18,6 @@ def set_emoji(teams, tournament): emojis = random.sample(unused_emoji, len(teams)) for team, emoji in zip(teams, emojis): - print(team, ord(emoji)) team.emoji = emoji team.save() diff --git a/tabbycat/participants/management/commands/generateemoji.py b/tabbycat/participants/management/commands/generateemoji.py index 85d0b56a9d9..d4ec378fc15 100644 --- a/tabbycat/participants/management/commands/generateemoji.py +++ b/tabbycat/participants/management/commands/generateemoji.py @@ -1,6 +1,6 @@ from utils.management.base import TournamentCommand -from ...emoji import pick_unused_emoji +from ...emoji import set_emoji class Command(TournamentCommand): @@ -10,8 +10,6 @@ class Command(TournamentCommand): def handle_tournament(self, tournament, **options): all_teams = tournament.team_set.all() all_teams.update(emoji=None) - for team in all_teams: - team.emoji = pick_unused_emoji(all_teams) - team.save() + set_emoji(all_teams, tournament) self.stdout.write("Assigned emoji to {count} teams in tournament {tournament}".format( count=all_teams.count(), tournament=tournament)) From d5a3a00bb9f81f3178b0c42625506d731db8d274 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 16:25:16 -0700 Subject: [PATCH 04/13] Refactor importer for #461 --- tabbycat/importer/anorak.py | 94 ++++++------------- tabbycat/importer/base.py | 87 +++++++---------- .../management/commands/importtournament.py | 9 +- 3 files changed, 70 insertions(+), 120 deletions(-) diff --git a/tabbycat/importer/anorak.py b/tabbycat/importer/anorak.py index c35046324fc..e6523b6629f 100644 --- a/tabbycat/importer/anorak.py +++ b/tabbycat/importer/anorak.py @@ -9,7 +9,7 @@ import tournaments.models as tm import tournaments.utils import venues.models as vm -from participants.emoji import pick_unused_emoji +from participants.emoji import set_emoji from .base import BaseTournamentDataImporter, make_interpreter, make_lookup @@ -72,21 +72,19 @@ def import_rounds(self, f): break_category=lambda x: bm.BreakCategory.objects.get( slug=x, tournament=self.tournament) ) - counts, errors = self._import(f, tm.Round, round_interpreter) + self._import(f, tm.Round, round_interpreter) # Set the round with the lowest known seqno to be the current round. self.tournament.current_round = self.tournament.round_set.order_by('seq').first() self.tournament.save() - return counts, errors - def import_regions(self, f): """Imports regions from a file. Each line has: name """ region_interpreter = make_interpreter() - return self._import(f, pm.Region, region_interpreter) + self._import(f, pm.Region, region_interpreter) def import_institutions(self, f, auto_create_regions=True): """Imports institutions from a file, also creating regions as needed @@ -94,6 +92,7 @@ def import_institutions(self, f, auto_create_regions=True): Each line has: name, code, abbreviation, region """ + if auto_create_regions: def region_interpreter(line): if not line.get('region'): @@ -101,20 +100,13 @@ def region_interpreter(line): return { 'name': line['region'] } - counts, errors = self._import(f, pm.Region, region_interpreter, - expect_unique=False) - else: - counts = None - errors = None + self._import(f, pm.Region, region_interpreter, expect_unique=False) institution_interpreter = make_interpreter( region=lambda x: pm.Region.objects.get(name=x) ) - counts, errors = self._import(f, pm.Institution, institution_interpreter, - counts=counts, errors=errors) - - return counts, errors + self._import(f, pm.Institution, institution_interpreter) def import_venues(self, f, auto_create_categories=True): """Imports venues from a file, also creating venue categories as needed @@ -131,16 +123,14 @@ def import_venues(self, f, auto_create_categories=True): DELETE=['category'], category=lambda x: vm.VenueCategory.objects.get(name=x), ) - counts, errors = self._import(f, vm.Venue, venue_interpreter) + self._import(f, vm.Venue, venue_interpreter) if auto_create_categories: def venue_category_interpreter(line): if not line.get('category'): return None return {'name': line['category']} - counts, errors = self._import( - f, vm.VenueCategory, venue_category_interpreter, - expect_unique=False, counts=counts, errors=errors) + self._import(f, vm.VenueCategory, venue_category_interpreter, expect_unique=False) def venue_category_venue_interpreter(line): if not line.get('category'): @@ -150,10 +140,7 @@ def venue_category_venue_interpreter(line): 'venue': vm.Venue.objects.get(name=line['name']) } - counts, errors = self._import(f, vm.VenueCategory.venues.through, - venue_category_venue_interpreter, counts=counts, errors=errors) - - return counts, errors + self._import(f, vm.VenueCategory.venues.through, venue_category_venue_interpreter) def import_venue_categories(self, f): """Imports venue categories from a file. @@ -165,10 +152,7 @@ def import_venue_categories(self, f): display_in_venue_name=self.lookup_venue_category_display, ) - counts, errors = self._import(f, vm.VenueCategory, - venue_category_interpreter, expect_unique=False) - - return counts, errors + self._import(f, vm.VenueCategory, venue_category_interpreter, expect_unique=False) def import_break_categories(self, f): """Imports break categories from a file. @@ -180,7 +164,7 @@ def import_break_categories(self, f): break_category_interpreter = make_interpreter( tournament=self.tournament, ) - return self._import(f, bm.BreakCategory, break_category_interpreter) + self._import(f, bm.BreakCategory, break_category_interpreter) def import_teams(self, f, create_dummy_speakers=False): """Imports teams from a file. If 'create_dummy_speakers' is True, @@ -196,9 +180,8 @@ def team_interpreter(line): line['short_reference'] = line['reference'][:34] return line - used_emoji = [] - counts, errors = self._import(f, pm.Team, team_interpreter, - generated_fields={'emoji': (lambda: pick_unused_emoji(used=used_emoji))}) + teams = self._import(f, pm.Team, team_interpreter) + set_emoji(teams, self.tournament) if create_dummy_speakers: def speakers_interpreter(line): @@ -206,8 +189,7 @@ def speakers_interpreter(line): institution=pm.Institution.objects.lookup(line['institution'])) for name in ["1st Speaker", "2nd Speaker", "3rd Speaker", "Reply Speaker"]: yield dict(name=name, team=team) - counts, errors = self._import(f, pm.Speaker, speakers_interpreter, - counts=counts, errors=errors) + self._import(f, pm.Speaker, speakers_interpreter) def import_speakers(self, f, auto_create_teams=True): """Imports speakers from a file, also creating teams as needed (unless @@ -232,13 +214,8 @@ def team_interpreter(line): interpreted['use_institution_prefix'] = line['use_institution_prefix'] return interpreted - used_emoji = [] - counts, errors = self._import(f, pm.Team, team_interpreter, expect_unique=False, - generated_fields={'emoji': (lambda: pick_unused_emoji(used=used_emoji))}) - - else: - counts = None - errors = None + teams = self._import(f, pm.Team, team_interpreter, expect_unique=False) + set_emoji(teams, self.tournament) speaker_interpreter_part = make_interpreter( DELETE=['use_institution_prefix', 'institution', 'team_name'], @@ -251,10 +228,7 @@ def speaker_interpreter(line): institution=institution, reference=line['team_name'], tournament=self.tournament) line = speaker_interpreter_part(line) return line - counts, errors = self._import(f, pm.Speaker, speaker_interpreter, - counts=counts, errors=errors) - - return counts, errors + self._import(f, pm.Speaker, speaker_interpreter) def import_adjudicators(self, f, auto_conflict=True): """Imports adjudicators from a file. Institutions are not created as @@ -274,7 +248,7 @@ def import_adjudicators(self, f, auto_conflict=True): gender=self.lookup_gender, DELETE=['team_conflicts', 'institution_conflicts', 'adj_conflicts'] ) - counts, errors = self._import(f, pm.Adjudicator, adjudicator_interpreter) + self._import(f, pm.Adjudicator, adjudicator_interpreter) def test_score_interpreter(line): institution = pm.Institution.objects.lookup(line['institution']) @@ -284,9 +258,7 @@ def test_score_interpreter(line): 'score' : line['test_score'], 'round' : None, } - counts, errors = self._import(f, fm.AdjudicatorTestScoreHistory, - test_score_interpreter, counts=counts, - errors=errors) + self._import(f, fm.AdjudicatorTestScoreHistory, test_score_interpreter) def own_institution_conflict_interpreter(line): institution = pm.Institution.objects.lookup(line['institution']) @@ -294,9 +266,7 @@ def own_institution_conflict_interpreter(line): 'adjudicator' : pm.Adjudicator.objects.get(name=line['name'], institution=institution, tournament=self.tournament), 'institution' : institution, } - counts, errors = self._import(f, am.AdjudicatorInstitutionConflict, - own_institution_conflict_interpreter, - counts=counts, errors=errors) + self._import(f, am.AdjudicatorInstitutionConflict, own_institution_conflict_interpreter) def institution_conflict_interpreter(line): if not line.get('institution_conflicts'): @@ -310,9 +280,7 @@ def institution_conflict_interpreter(line): 'adjudicator' : adjudicator, 'institution' : institution, } - counts, errors = self._import(f, am.AdjudicatorInstitutionConflict, - institution_conflict_interpreter, - counts=counts, errors=errors) + self._import(f, am.AdjudicatorInstitutionConflict, institution_conflict_interpreter) def team_conflict_interpreter(line): if not line.get('team_conflicts'): @@ -327,9 +295,7 @@ def team_conflict_interpreter(line): 'adjudicator' : adjudicator, 'team' : team, } - counts, errors = self._import(f, am.AdjudicatorConflict, - team_conflict_interpreter, - counts=counts, errors=errors) + self._import(f, am.AdjudicatorConflict, team_conflict_interpreter) def adj_conflict_interpreter(line): if not line.get('adj_conflicts'): @@ -344,11 +310,7 @@ def adj_conflict_interpreter(line): 'adjudicator' : adjudicator, 'conflict_adjudicator' : conflicted_adj, } - counts, errors = self._import(f, am.AdjudicatorAdjudicatorConflict, - adj_conflict_interpreter, - counts=counts, errors=errors) - - return counts, errors + self._import(f, am.AdjudicatorAdjudicatorConflict, adj_conflict_interpreter) def import_motions(self, f): """Imports motions from a file. @@ -358,7 +320,7 @@ def import_motions(self, f): motions_interpreter = make_interpreter( round=lambda x: tm.Round.objects.lookup(x, tournament=self.tournament), ) - return self._import(f, mm.Motion, motions_interpreter) + self._import(f, mm.Motion, motions_interpreter) def import_sides(self, f): """Imports sides from a file. @@ -374,7 +336,7 @@ def side_interpreter(line): 'team' : team, 'position' : self.lookup_team_position(side), } - return self._import(f, dm.TeamPositionAllocation, side_interpreter) + self._import(f, dm.TeamPositionAllocation, side_interpreter) def import_adj_feedback_questions(self, f): """Imports adjudicator feedback questions from a file. @@ -387,7 +349,7 @@ def import_adj_feedback_questions(self, f): answer_type=self.lookup_feedback_answer_type, ) - return self._import(f, fm.AdjudicatorFeedbackQuestion, question_interpreter) + self._import(f, fm.AdjudicatorFeedbackQuestion, question_interpreter) def import_adj_venue_constraints(self, f): """Imports venue constraints from a file. @@ -406,7 +368,7 @@ def adj_venue_constraints_interpreter(line): del line['adjudicator'] return line - return self._import(f, vm.VenueConstraint, adj_venue_constraints_interpreter) + self._import(f, vm.VenueConstraint, adj_venue_constraints_interpreter) def import_team_venue_constraints(self, f): """Imports venue constraints from a file. @@ -425,7 +387,7 @@ def team_venue_constraints_interpreter(line): del line['team'] return line - return self._import(f, vm.VenueConstraint, team_venue_constraints_interpreter) + self._import(f, vm.VenueConstraint, team_venue_constraints_interpreter) def auto_make_rounds(self, num_rounds): """Makes the number of rounds specified. The first one is random and the diff --git a/tabbycat/importer/base.py b/tabbycat/importer/base.py index 17fdab100a2..60dc05247f1 100644 --- a/tabbycat/importer/base.py +++ b/tabbycat/importer/base.py @@ -118,8 +118,7 @@ def import_things(self, f): interpreter = make_interpreter( institution=lambda x: participants.models.Institution.objects.get(name=x) ) - counts, errors = self._import(f, participants.models.Speaker, interpreter) - return counts, errors + self._import(f, participants.models.Speaker, interpreter) See the documentation for _import for more details. """ @@ -132,9 +131,11 @@ def __init__(self, tournament, **kwargs): self.logger.setLevel(kwargs['loglevel']) self.expect_unique = kwargs.get('expect_unique', True) - def _import(self, csvfile, model, interpreter=make_interpreter(), - counts=None, errors=None, expect_unique=None, - generated_fields={}): + def reset_counts(self): + self.counts = Counter() + self.errors = TournamentDataImporterError() + + def _import(self, csvfile, model, interpreter=make_interpreter(), expect_unique=None): """Parses the object given in f, using the callable line_parser to parse each line, and passing the arguments to the given model's constructor. `csvfile` can be any object that is supported by csv.reader(), which @@ -152,41 +153,28 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), each dict yielded. If omitted, the dict returned by `csv.DictReader` will be used directly. - Returns a tuple of two objects. The first is a Counter object (from - Python's collections module) in which the keys are models (e.g. Round, - Team) and the values are the number that were created. The second is a - (possibly empty) TournamentDataImporterError object. If self.strict is - True, then the returned TournamentDataImporterError will always be empty - (since otherwise it would have raised it as an exception); otherwise, it - will contain the errors raised during the import attempt. - - If `counts` and/or `errors` are provided, this function adds the counts - and errors from this _import call and returns them instead. This - modifies the original counts_in and errors_in. This allows easy daisy- - chaining of successive _import calls. If provided, 'counts_in' should - behave like a Counter object and 'errors_in' should behave like a - TournamentDataImporterError object. + Returns a list of instances created in this import. + + Callers may also access two attributes, which are updated every time + this function is called. The first, `self.counts` is a Counter object + (from Python's collections module) in which the keys are models (e.g. + Round, Team) and the values are the number that were created. The + second, `self.errors` is a (possibly empty) TournamentDataImporterError + object. If `self.strict` is True, then `self.errors` will always be + empty (since otherwise it would have raised it as an exception); + otherwise, it will contain the errors raised during the import attempt. If `expect_unique` is True, this function checks that there are no duplicate objects before saving any of the objects it creates. If `expect_unique` is False, it will just skip objects that would be duplicates and log a DUPLICATE_INFO message to say so. - - If `generated_fields` is given, it must be a dict, with keys being field - names and values being callables. The uniqueness checks will not take - into account any of the generated fields. This should be used for fields - that are generated with each object, not given in the CSV files. """ if hasattr(csvfile, 'seek') and callable(csvfile.seek): csvfile.seek(0) reader = csv.DictReader(csvfile) kwargs_seen = list() - insts = list() - if counts is None: - counts = Counter() - if errors is None: - errors = TournamentDataImporterError() - _errors = TournamentDataImporterError() # keep this run's errors separate + instances = list() + errors = TournamentDataImporterError() if expect_unique is None: expect_unique = self.expect_unique skipped_because_existing = 0 @@ -205,7 +193,7 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), except (ObjectDoesNotExist, MultipleObjectsReturned, ValueError, TypeError, IndexError) as e: message = "Couldn't parse line: " + str(e) - _errors.add(lineno, model, message) + errors.add(lineno, model, message) continue if kwargs_list is None: @@ -221,16 +209,12 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), if kwargs_expect_unique in kwargs_seen: if expect_unique: message = "Duplicate " + description - _errors.add(lineno, model, message) + errors.add(lineno, model, message) else: self.logger.log(DUPLICATE_INFO, "Skipping duplicate " + description) continue kwargs_seen.append(kwargs_expect_unique) - # Fill in the generated fields - for key, generator_fn in generated_fields.items(): - kwargs[key] = generator_fn() - # Retrieve the instance or create it if it doesn't exist try: inst = model.objects.get(**kwargs) @@ -238,7 +222,7 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), inst = model(**kwargs) except MultipleObjectsReturned as e: if expect_unique: - _errors.add(lineno, model, str(e)) + errors.add(lineno, model, str(e)) continue except FieldError as e: match = re.match("Cannot resolve keyword '(\w+)' into field.", str(e)) @@ -254,16 +238,16 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), else: raise except ValueError as e: - _errors.add(lineno, model, str(e)) + errors.add(lineno, model, str(e)) continue except ValidationError as e: - _errors.update_with_validation_error(lineno, model, e) + errors.update_with_validation_error(lineno, model, e) continue else: skipped_because_existing += 1 if expect_unique: message = description + " already exists" - _errors.add(lineno, model, message) + errors.add(lineno, model, message) else: self.logger.log(DUPLICATE_INFO, "Skipping %s, already exists", description) continue @@ -271,29 +255,30 @@ def _import(self, csvfile, model, interpreter=make_interpreter(), try: inst.full_clean() except ValidationError as e: - _errors.update_with_validation_error(lineno, model, e) + errors.update_with_validation_error(lineno, model, e) continue self.logger.debug("Listing to create: " + description) - insts.append(inst) + instances.append(inst) - if _errors: + if errors: if self.strict: - for message in _errors.itermessages(): + for message in errors.itermessages(): self.logger.error(message) - raise _errors + raise errors else: - for message in _errors.itermessages(): + for message in errors.itermessages(): self.logger.warning(message) - errors.update(_errors) + self.errors.update(errors) - for inst in insts: + for inst in instances: self.logger.debug("Made %s: %r", model._meta.verbose_name, inst) inst.save() - self.logger.info("Imported %d %s", len(insts), model._meta.verbose_name_plural) + self.logger.info("Imported %d %s", len(instances), model._meta.verbose_name_plural) if skipped_because_existing: self.logger.info("(skipped %d %s)", skipped_because_existing, model._meta.verbose_name_plural) - counts.update({model: len(insts)}) - return counts, errors + self.counts.update({model: len(instances)}) + + return instances diff --git a/tabbycat/importer/management/commands/importtournament.py b/tabbycat/importer/management/commands/importtournament.py index e12d578eb2d..10b6fa26419 100644 --- a/tabbycat/importer/management/commands/importtournament.py +++ b/tabbycat/importer/management/commands/importtournament.py @@ -81,8 +81,10 @@ def _print_stage(self, message): message = "\033[0;36m" + message + "\033[0m\n" self.stdout.write(message) - def _print_result(self, counts, errors): + def _print_result(self): if self.verbosity > 0: + counts = self.importer.counts + errors = self.importer.errors if errors: for message in errors.itermessages(): if self.color: @@ -125,11 +127,12 @@ def _make(self, filename, import_method=None): import_method = getattr(self.importer, 'import_' + filename) if f is not None: self._print_stage("Importing %s.csv" % filename) + self.importer.reset_counts() try: - counts, errors = import_method(f) + import_method(f) except TournamentDataImporterFatal as e: raise CommandError(e) - self._print_result(counts, errors) + self._print_result() def get_data_path(self, arg): """Returns the directory for the given command-line argument. If the From 871490f419024999e211d6fac142055d7cd139aa Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 16:38:13 -0700 Subject: [PATCH 05/13] Convert simple importer #461 --- tabbycat/importer/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tabbycat/importer/views.py b/tabbycat/importer/views.py index bc21a5bb017..f0017368983 100644 --- a/tabbycat/importer/views.py +++ b/tabbycat/importer/views.py @@ -13,6 +13,7 @@ from formtools.wizard.views import SessionWizardView +from participants.emoji import set_emoji from participants.models import Adjudicator, Institution, Speaker, Team from tournaments.mixins import TournamentMixin from utils.misc import reverse_tournament @@ -76,9 +77,9 @@ def get_form(self, step=None, **kwargs): return form def done(self, form_list, form_dict, **kwargs): - instances = form_dict[self.DETAILS_STEP].save() + self.instances = form_dict[self.DETAILS_STEP].save() messages.success(self.request, _("Added %(count)d %(model_plural)s.") % { - 'count': len(instances), 'model_plural': self.model._meta.verbose_name_plural}) + 'count': len(self.instances), 'model_plural': self.model._meta.verbose_name_plural}) return HttpResponseRedirect(self.get_redirect_url()) @@ -146,6 +147,11 @@ class ImportTeamsWizardView(BaseImportByInstitutionWizardView): def get_details_instance_initial(self, i): return {'reference': str(i), 'use_institution_prefix': True} + def done(self, form_list, form_dict, **kwargs): + # Also set emoji on teams + redirect = super().done(form_list, form_dict, **kwargs) + set_emoji(self.instances, self.get_tournament()) + return redirect class ImportAdjudicatorsWizardView(BaseImportByInstitutionWizardView): model = Adjudicator From 1bef45ae98ed53355fdd5634807190dd15dd370d Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 16:39:59 -0700 Subject: [PATCH 06/13] flake8 --- tabbycat/importer/views.py | 1 + tabbycat/participants/admin.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tabbycat/importer/views.py b/tabbycat/importer/views.py index f0017368983..2113b38b275 100644 --- a/tabbycat/importer/views.py +++ b/tabbycat/importer/views.py @@ -153,6 +153,7 @@ def done(self, form_list, form_dict, **kwargs): set_emoji(self.instances, self.get_tournament()) return redirect + class ImportAdjudicatorsWizardView(BaseImportByInstitutionWizardView): model = Adjudicator form_list = [ diff --git a/tabbycat/participants/admin.py b/tabbycat/participants/admin.py index 13006c5c6ba..e03913d871b 100644 --- a/tabbycat/participants/admin.py +++ b/tabbycat/participants/admin.py @@ -1,4 +1,3 @@ -import random from django.contrib import admin from django import forms From 9b68b51be0e6f652b7401a19b24720791d123bdd Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 16:41:31 -0700 Subject: [PATCH 07/13] Add migration #461 --- .../migrations/0028_auto_20170522_0940.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tabbycat/participants/migrations/0028_auto_20170522_0940.py diff --git a/tabbycat/participants/migrations/0028_auto_20170522_0940.py b/tabbycat/participants/migrations/0028_auto_20170522_0940.py new file mode 100644 index 00000000000..b29e0bccee5 --- /dev/null +++ b/tabbycat/participants/migrations/0028_auto_20170522_0940.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-22 09:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('participants', '0027_auto_20170422_1511'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='emoji', + field=models.CharField(blank=True, choices=[('☕', '☕'), ('⛑', '⛑'), ('⛰', '⛰'), ('⛪', '⛪'), ('⛵', '⛵'), ('⛔', '⛔'), ('⛅', '⛅'), ('⛈', '⛈'), ('⛱', '⛱'), ('⛄', '⛄'), ('⚽', '⚽'), ('⛸', ''), ('⛏', '⛏'), ('😁', '😁'), ('😂', '😂'), ('😄', '😄'), ('😅', ''), ('😆', '😆'), ('😉', '😉'), ('😊', '😊'), ('😎', '😎'), ('😍', '😍'), ('😘', '😘'), ('😇', '😇'), ('😐', '😐'), ('😏', '😏'), ('😣', ''), ('😥', '😥'), ('😫', ''), ('😜', '😜'), ('😓', ''), ('😔', ''), ('😖', '😖'), ('😷', '😷'), ('😲', '😲'), ('😞', '😞'), ('😭', '😭'), ('😰', '😰'), ('😱', '😱'), ('😳', '😳'), ('😵', '😵'), ('😡', '😡'), ('👿', '👿'), ('👩', '👩'), ('👴', '👴'), ('👵', '👵'), ('👶', '👶'), ('👮', '👮'), ('👷', '👷'), ('👸', '👸'), ('💂', '💂'), ('🎅', '🎅'), ('👼', '👼'), ('👰', '👰'), ('🙅', '🙅'), ('🙆', '🙆'), ('🙋', '🙋'), ('🙇', '🙇'), ('🙌', '🙌'), ('🙏', '🙏'), ('💃', '💃'), ('💑', '💑'), ('👪', '👪'), ('👫', '👫'), ('👬', '👬'), ('👭', '👭'), ('💪', '💪'), ('👆', '👆'), ('✊', '✊'), ('✋', '✋'), ('👊', '👊'), ('👌', '👌'), ('👍', '👍'), ('👎', '👎'), ('👐', '👐'), ('💅', '💅'), ('👂', '👂'), ('👃', '👃'), ('👅', '👅'), ('👄', '👄'), ('💘', '💘'), ('💔', '💔'), ('💖', '💖'), ('💌', '💌'), ('💧', '💧'), ('💤', ''), ('💣', '💣'), ('💥', '💥'), ('💦', '💦'), ('💨', '💨'), ('💫', ''), ('👓', '👓'), ('👔', '👔'), ('👙', '👙'), ('👜', '👜'), ('👟', '👟'), ('👠', '👠'), ('👒', '👒'), ('🎩', '🎩'), ('💄', '💄'), ('💍', '💍'), ('💎', '💎'), ('👻', '👻'), ('💀', '💀'), ('👽', '👽'), ('👾', '👾'), ('💩', '💩'), ('🐵', ''), ('🙈', ''), ('🙉', ''), ('🙊', ''), ('🐶', '🐶'), ('🐩', ''), ('🐯', '🐯'), ('🐴', '🐴'), ('🐮', '🐮'), ('🐷', '🐷'), ('🐑', '🐑'), ('🐭', '🐭'), ('🐹', '🐹'), ('🐰', '🐰'), ('🐻', '🐻'), ('🐨', '🐨'), ('🐼', '🐼'), ('🐔', '🐔'), ('🐥', ''), ('🐦', '🐦'), ('🐧', '🐧'), ('🐸', '🐸'), ('🐢', ''), ('🐍', '🐍'), ('🐲', '🐲'), ('🐳', '🐳'), ('🐬', ''), ('🐟', '🐟'), ('🐠', ''), ('🐙', '🐙'), ('🐚', '🐚'), ('🐌', ''), ('🐛', ''), ('🐝', '🐝'), ('💐', ''), ('🌸', '🌸'), ('🌹', '🌹'), ('🌻', '🌻'), ('🌷', '🌷'), ('🌱', ''), ('🌴', ''), ('🌵', '🌵'), ('🌿', ''), ('🍀', ''), ('🍁', '🍁'), ('🍇', '🍇'), ('🍉', '🍉'), ('🍊', '🍊'), ('🍋', '🍋'), ('🍌', '🍌'), ('🍍', '🍍'), ('🍎', '🍎'), ('🍑', '🍑'), ('🍒', '🍒'), ('🍓', '🍓'), ('🍅', '🍅'), ('🍆', '🍆'), ('🌽', '🌽'), ('🍄', '🍄'), ('🍞', '🍞'), ('🍔', '🍔'), ('🍟', ''), ('🍕', '🍕'), ('🍙', ''), ('🍨', '🍨'), ('🍩', '🍩'), ('🍪', '🍪'), ('🍰', '🍰'), ('🍭', '🍭'), ('🍼', '🍼'), ('🍷', '🍷'), ('🍸', '🍸'), ('🍹', '🍹'), ('🍺', '🍺'), ('🍴', '🍴'), ('🌋', '🌋'), ('🏠', '🏠'), ('🏢', '🏢'), ('🏥', ''), ('🏩', '🏩'), ('🏰', ''), ('🌊', '🌊'), ('🎡', ''), ('🎢', ''), ('🎨', '🎨'), ('🚃', '🚃'), ('🚄', '🚄'), ('🚝', '🚝'), ('🚍', '🚍'), ('🚔', '🚔'), ('🚘', '🚘'), ('🚲', '🚲'), ('🚨', '🚨'), ('🚣', '🚣'), ('🚁', '🚁'), ('🚀', '🚀'), ('🚦', '🚦'), ('🚧', '🚧'), ('🚫', '🚫'), ('🚷', '🚷'), ('🚻', '🚻'), ('🚽', '🚽'), ('🚿', '🚿'), ('🛀', '🛀'), ('⏳', '⏳'), ('🌑', '🌑'), ('🌕', '🌕'), ('🌗', '🌗'), ('🌞', '🌞'), ('🌈', '🌈'), ('🌂', '🌂'), ('🌟', '🌟'), ('🔥', '🔥'), ('🎃', '🎃'), ('🎄', '🎄'), ('🎈', '🎈'), ('🎉', '🎉'), ('🎓', '🎓'), ('🎯', '🎯'), ('🎀', '🎀'), ('🏀', '🏀'), ('🏈', '🏈'), ('🎾', '🎾'), ('🎱', '🎱'), ('🏊', ''), ('🎮', '🎮'), ('🎲', '🎲'), ('📣', '📣'), ('📯', ''), ('🔔', '🔔'), ('🎶', '🎶'), ('🎤', '🎤'), ('🎹', '🎹'), ('🎺', '🎺'), ('🎻', '🎻'), ('📻', '📻'), ('📱', '📱'), ('📞', '📞'), ('🔋', '🔋'), ('🔌', '🔌'), ('💾', '💾'), ('💿', '💿'), ('🎬', '🎬'), ('📷', '📷'), ('🔍', '🔍'), ('🔭', '🔭'), ('💡', '💡'), ('📕', '📕'), ('📰', '📰'), ('💰', '💰'), ('💸', '💸'), ('📦', ''), ('📫', '📫'), ('💼', '💼'), ('📅', '📅'), ('📎', ''), ('📏', '📏'), ('📐', '📐'), ('🔑', '🔑'), ('🔩', '🔩'), ('💊', ''), ('🔪', '🔪'), ('🔫', '🔫'), ('🚬', '🚬'), ('🏁', ''), ('🔮', '🔮'), ('❌', '❌'), ('❓', '❓'), ('🔞', '🔞'), ('🆒', '🆒'), ('🆗', '🆗'), ('🆘', '🆘'), ('😙', '😙'), ('😑', '😑'), ('😮', '😮'), ('😴', '😴'), ('😛', '😛'), ('😧', '😧'), ('😬', '😬'), ('\U0001f575', '\U0001f575'), ('\U0001f595', '\U0001f595'), ('\U0001f596', '\U0001f596'), ('\U0001f441', '\U0001f441'), ('\U0001f576', '\U0001f576'), ('\U0001f6cd', '\U0001f6cd'), ('\U0001f43f', '\U0001f43f'), ('\U0001f54a', '\U0001f54a'), ('\U0001f577', '\U0001f577'), ('\U0001f336', '\U0001f336'), ('\U0001f3d5', ''), ('\U0001f3db', '\U0001f3db'), ('\U0001f6e2', '\U0001f6e2'), ('\U0001f6e5', ''), ('\U0001f6e9', ''), ('\U0001f6ce', '\U0001f6ce'), ('\U0001f570', '\U0001f570'), ('\U0001f321', '\U0001f321'), ('\U0001f324', '\U0001f324'), ('\U0001f327', '\U0001f327'), ('\U0001f329', '\U0001f329'), ('\U0001f32a', '\U0001f32a'), ('\U0001f32c', '\U0001f32c'), ('\U0001f396', '\U0001f396'), ('\U0001f397', '\U0001f397'), ('\U0001f39e', '\U0001f39e'), ('\U0001f3cb', ''), ('\U0001f3c5', '\U0001f3c5'), ('\U0001f579', '\U0001f579'), ('\U0001f399', '\U0001f399'), ('\U0001f5a5', '\U0001f5a5'), ('\U0001f5a8', '\U0001f5a8'), ('\U0001f5b2', '\U0001f5b2'), ('\U0001f4f8', ''), ('\U0001f56f', '\U0001f56f'), ('\U0001f5de', ''), ('\U0001f58b', '\U0001f58b'), ('\U0001f5d1', '\U0001f5d1'), ('\U0001f6e0', ''), ('\U0001f5e1', '\U0001f5e1'), ('\U0001f6e1', '\U0001f6e1'), ('\U0001f3f3', '\U0001f3f3'), ('\U0001f3f4', '\U0001f3f4'), ('\U0001f917', '\U0001f917'), ('\U0001f914', '\U0001f914'), ('\U0001f644', '\U0001f644'), ('\U0001f910', '\U0001f910'), ('\U0001f913', '\U0001f913'), ('\U0001f643', '\U0001f643'), ('\U0001f912', '\U0001f912'), ('\U0001f915', '\U0001f915'), ('\U0001f911', '\U0001f911'), ('\U0001f918', '\U0001f918'), ('\U0001f4ff', '\U0001f4ff'), ('\U0001f916', '\U0001f916'), ('\U0001f981', '\U0001f981'), ('\U0001f984', '\U0001f984'), ('\U0001f980', '\U0001f980'), ('\U0001f982', ''), ('\U0001f9c0', '\U0001f9c0'), ('\U0001f32d', '\U0001f32d'), ('\U0001f32e', '\U0001f32e'), ('\U0001f37f', '\U0001f37f'), ('\U0001f37e', '\U0001f37e'), ('\U0001f3cf', '\U0001f3cf'), ('\U0001f3d0', '\U0001f3d0'), ('\U0001f3d3', '\U0001f3d3'), ('\U0001f3f9', '\U0001f3f9'), ('\U0001f923', '\U0001f923'), ('\U0001f924', '\U0001f924'), ('\U0001f922', '\U0001f922'), ('\U0001f927', '\U0001f927'), ('\U0001f920', '\U0001f920'), ('\U0001f921', '\U0001f921'), ('\U0001f925', '\U0001f925'), ('\U0001f934', '\U0001f934'), ('\U0001f935', '\U0001f935'), ('\U0001f930', '\U0001f930'), ('\U0001f936', '\U0001f936'), ('\U0001f926', '\U0001f926'), ('\U0001f937', '\U0001f937'), ('\U0001f57a', '\U0001f57a'), ('\U0001f93a', '\U0001f93a'), ('\U0001f938', '\U0001f938'), ('\U0001f939', '\U0001f939'), ('\U0001f933', '\U0001f933'), ('\U0001f91e', '\U0001f91e'), ('\U0001f919', '\U0001f919'), ('\U0001f91b', '\U0001f91b'), ('\U0001f91c', '\U0001f91c'), ('\U0001f91a', '\U0001f91a'), ('\U0001f91d', '\U0001f91d'), ('\U0001f5a4', '\U0001f5a4'), ('\U0001f98a', '\U0001f98a'), ('\U0001f98c', '\U0001f98c'), ('\U0001f987', '\U0001f987'), ('\U0001f985', '\U0001f985'), ('\U0001f986', '\U0001f986'), ('\U0001f989', '\U0001f989'), ('\U0001f98e', '\U0001f98e'), ('\U0001f988', '\U0001f988'), ('\U0001f990', '\U0001f990'), ('\U0001f991', '\U0001f991'), ('\U0001f98b', '\U0001f98b'), ('\U0001f940', '\U0001f940'), ('\U0001f95d', '\U0001f95d'), ('\U0001f951', '\U0001f951'), ('\U0001f954', '\U0001f954'), ('\U0001f955', '\U0001f955'), ('\U0001f952', '\U0001f952'), ('\U0001f95c', '\U0001f95c'), ('\U0001f950', '\U0001f950'), ('\U0001f956', '\U0001f956'), ('\U0001f95e', '\U0001f95e'), ('\U0001f959', '\U0001f959'), ('\U0001f95a', '\U0001f95a'), ('\U0001f957', '\U0001f957'), ('\U0001f95b', '\U0001f95b'), ('\U0001f942', '\U0001f942'), ('\U0001f943', '\U0001f943'), ('\U0001f944', '\U0001f944'), ('\U0001f6f6', '\U0001f6f6'), ('\U0001f94a', '\U0001f94a'), ('\U0001f94b', '\U0001f94b'), ('\U0001f945', '\U0001f945'), ('\U0001f941', '\U0001f941'), ('\U0001f6d2', '\U0001f6d2')], default=None, max_length=2, null=True), + ), + ] From 8451da6f9980003a667a04d5a5b6309a7f377b24 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 16:44:34 -0700 Subject: [PATCH 08/13] Remove obsolete emoji references #461 --- tabbycat/participants/emoji.py | 16 ++++------------ tabbycat/participants/models.py | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tabbycat/participants/emoji.py b/tabbycat/participants/emoji.py index 02a4c62a1e7..4258d6402e3 100644 --- a/tabbycat/participants/emoji.py +++ b/tabbycat/participants/emoji.py @@ -22,7 +22,7 @@ def set_emoji(teams, tournament): team.save() -def pick_unused_emoji(teams=None, used=None): +def pick_unused_emoji(): """Picks an emoji that is not already in use by any team in `teams`. If `teams` is not specified, it picks an emoji not in use by any team in the database. If no emoji are left, it returns `None`. @@ -30,20 +30,12 @@ def pick_unused_emoji(teams=None, used=None): If `used` is specified, it should be a list of emoji, and it also avoids emoji in `used` and appends the chosen emoji to the list. """ - if teams is None: - from .models import Team - teams = Team.objects.all() - - used_emoji = teams.filter(emoji__isnull=False).values_list('emoji', flat=True) - if used is not None: - used_emoji = list(used_emoji) + used + from .models import Team + used_emoji = Team.objects.filter(emoji__isnull=False).values_list('emoji', flat=True) unused_emoji = [e[0] for e in EMOJI_LIST if e[0] not in used_emoji] try: - emoji = random.choice(unused_emoji) - if used is not None: - used.append(emoji) - return emoji + return random.choice(unused_emoji) except IndexError: return None diff --git a/tabbycat/participants/models.py b/tabbycat/participants/models.py index ec479b87665..12c491040d6 100644 --- a/tabbycat/participants/models.py +++ b/tabbycat/participants/models.py @@ -10,7 +10,7 @@ from tournaments.models import Round from utils.managers import LookupByNameFieldsMixin -from .emoji import EMOJI_LIST, pick_unused_emoji +from .emoji import EMOJI_LIST logger = logging.getLogger(__name__) @@ -129,7 +129,7 @@ class Team(models.Model): emoji = models.CharField(max_length=2, blank=True, null=True, default=None, choices=EMOJI_LIST) # uses null=True to allow multiple teams to have no emoji - construct_emoji = pick_unused_emoji # historical reference for migration 0026_auto_20170416_2332 + construct_emoji = None # historical reference for migration 0026_auto_20170416_2332 class Meta: unique_together = [ From dc0b59740b8c7c6cc6bec7d762c48281e76caa14 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 17:01:00 -0700 Subject: [PATCH 09/13] Hopefully fix tests #461 --- tabbycat/importer/base.py | 1 + tabbycat/importer/tests/test_anorak.py | 86 +++++++++++++------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/tabbycat/importer/base.py b/tabbycat/importer/base.py index 60dc05247f1..281f90baacb 100644 --- a/tabbycat/importer/base.py +++ b/tabbycat/importer/base.py @@ -130,6 +130,7 @@ def __init__(self, tournament, **kwargs): if 'loglevel' in kwargs: self.logger.setLevel(kwargs['loglevel']) self.expect_unique = kwargs.get('expect_unique', True) + self.reset_counts() def reset_counts(self): self.counts = Counter() diff --git a/tabbycat/importer/tests/test_anorak.py b/tabbycat/importer/tests/test_anorak.py index 86ed706a9a5..9d33adbabd9 100644 --- a/tabbycat/importer/tests/test_anorak.py +++ b/tabbycat/importer/tests/test_anorak.py @@ -49,16 +49,16 @@ def assertCountsDictEqual(self, counts, expected): # noqa def test_break_categories(self): f = self._open_csv_file(self.TESTDIR, "break_categories") - counts, errors = self.importer.import_break_categories(f) - self.assertCountsDictEqual(counts, {bm.BreakCategory: 3}) - self.assertFalse(errors) + self.importer.import_break_categories(f) + self.assertCountsDictEqual(self.counts, {bm.BreakCategory: 3}) + self.assertFalse(self.errors) def test_rounds(self): self.test_break_categories() f = self._open_csv_file(self.TESTDIR, "rounds") - counts, errors = self.importer.import_rounds(f) - self.assertCountsDictEqual(counts, {tm.Round: 10}) - self.assertFalse(errors) + self.importer.import_rounds(f) + self.assertCountsDictEqual(self.counts, {tm.Round: 10}) + self.assertFalse(self.errors) def test_auto_make_rounds(self): self.importer.auto_make_rounds(7) @@ -66,84 +66,84 @@ def test_auto_make_rounds(self): def test_venues(self): f = self._open_csv_file(self.TESTDIR, "venues") - counts, errors = self.importer.import_venues(f) - self.assertCountsDictEqual(counts, {vm.VenueCategory: 8, vm.Venue: 23, + self.importer.import_venues(f) + self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 8, vm.Venue: 23, vm.VenueCategory.venues.through: 22}) - self.assertFalse(errors) + self.assertFalse(self.errors) def test_institutions(self): f = self._open_csv_file(self.TESTDIR, "institutions") - counts, errors = self.importer.import_institutions(f) - self.assertCountsDictEqual(counts, {pm.Institution: 13, pm.Region: 6}) - self.assertFalse(errors) + self.importer.import_institutions(f) + self.assertCountsDictEqual(self.counts, {pm.Institution: 13, pm.Region: 6}) + self.assertFalse(self.errors) @skip("test file does not yet exist") def test_teams(self): f = self._open_csv_file(self.TESTDIR, "teams") # noqa - counts, errors = self.importer.import_teams(self) - self.assertCountsDictEqual(counts, {pm.Team: 12}) - self.assertFalse(errors) + self.importer.import_teams(self) + self.assertCountsDictEqual(self.counts, {pm.Team: 12}) + self.assertFalse(self.errors) def test_speakers(self): self.test_institutions() f = self._open_csv_file(self.TESTDIR, "speakers") - counts, errors = self.importer.import_speakers(f) - self.assertCountsDictEqual(counts, {pm.Team: 24, pm.Speaker: 72}) - self.assertFalse(errors) + self.importer.import_speakers(f) + self.assertCountsDictEqual(self.counts, {pm.Team: 24, pm.Speaker: 72}) + self.assertFalse(self.errors) def test_adjudicators(self): self.test_speakers() f = self._open_csv_file(self.TESTDIR, "judges") - counts, errors = self.importer.import_adjudicators(f) - self.assertCountsDictEqual(counts, { + self.importer.import_adjudicators(f) + self.assertCountsDictEqual(self.counts, { pm.Adjudicator: 27, fm.AdjudicatorTestScoreHistory: 24, am.AdjudicatorInstitutionConflict: 36, am.AdjudicatorAdjudicatorConflict: 5, am.AdjudicatorConflict: 8, }) - self.assertFalse(errors) + self.assertFalse(self.errors) def test_motions(self): self.test_rounds() f = self._open_csv_file(self.TESTDIR, "motions") - counts, errors = self.importer.import_motions(f) - self.assertCountsDictEqual(counts, {mm.Motion: 18}) - self.assertFalse(errors) + self.importer.import_motions(f) + self.assertCountsDictEqual(self.counts, {mm.Motion: 18}) + self.assertFalse(self.errors) def test_adj_feedback_questions(self): f = self._open_csv_file(self.TESTDIR, "questions") - counts, errors = self.importer.import_adj_feedback_questions(f) - self.assertCountsDictEqual(counts, {fm.AdjudicatorFeedbackQuestion: 11}) - self.assertFalse(errors) + self.importer.import_adj_feedback_questions(f) + self.assertCountsDictEqual(self.counts, {fm.AdjudicatorFeedbackQuestion: 11}) + self.assertFalse(self.errors) def test_venue_categories(self): f = self._open_csv_file(self.TESTDIR, "venue_categories") - counts, errors = self.importer.import_venue_categories(f) - self.assertCountsDictEqual(counts, {vm.VenueCategory: 8}) - self.assertFalse(errors) + self.importer.import_venue_categories(f) + self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 8}) + self.assertFalse(self.errors) def test_adj_venue_constraints(self): self.test_venue_categories() self.test_adjudicators() f = self._open_csv_file(self.TESTDIR, "adj_venue_constraints") - counts, errors = self.importer.import_adj_venue_constraints(f) - self.assertCountsDictEqual(counts, {vm.VenueConstraint: 3}) - self.assertFalse(errors) + self.importer.import_adj_venue_constraints(f) + self.assertCountsDictEqual(self.counts, {vm.VenueConstraint: 3}) + self.assertFalse(self.errors) def test_team_venue_constraints(self): self.test_venue_categories() self.test_speakers() f = self._open_csv_file(self.TESTDIR, "team_venue_constraints") - counts, errors = self.importer.import_team_venue_constraints(f) - self.assertCountsDictEqual(counts, {vm.VenueConstraint: 2}) - self.assertFalse(errors) + self.importer.import_team_venue_constraints(f) + self.assertCountsDictEqual(self.counts, {vm.VenueConstraint: 2}) + self.assertFalse(self.errors) def test_invalid_line(self): self.test_speakers() f = self._open_csv_file(self.TESTDIR_ERRORS, "judges_invalid_line") with self.assertRaises(TournamentDataImporterError) as raisescm, self.assertLogs(self.logger, logging.ERROR) as logscm: - counts, errors = self.importer.import_adjudicators(f) + self.importer.import_adjudicators(f) self.assertCountEqual([e.lineno for e in raisescm.exception.entries], (2, 5, 9, 10, 15, 16, 23, 24, 26, 28)) self.assertEqual(len(raisescm.exception), 10) self.assertEqual(len(logscm.records), 10) @@ -151,15 +151,15 @@ def test_invalid_line(self): def test_weird_choices_judges(self): self.test_speakers() f = self._open_csv_file(self.TESTDIR_CHOICES, "judges") - counts, errors = self.importer.import_adjudicators(f) - self.assertCountsDictEqual(counts, { + self.importer.import_adjudicators(f) + self.assertCountsDictEqual(self.counts, { pm.Adjudicator: 27, am.AdjudicatorAdjudicatorConflict: 0, fm.AdjudicatorTestScoreHistory: 27, am.AdjudicatorInstitutionConflict: 36, am.AdjudicatorConflict: 7, }) - self.assertFalse(errors) + self.assertFalse(self.errors) def test_blank_entry_strict(self): f = self._open_csv_file(self.TESTDIR_ERRORS, "venues") @@ -176,12 +176,12 @@ def test_blank_entry_not_strict(self): f = self._open_csv_file(self.TESTDIR_ERRORS, "venues") self.importer.strict = False with self.assertLogs(self.logger, logging.WARNING) as logscm: - counts, errors = self.importer.import_venues(f) - self.assertCountsDictEqual(counts, {vm.VenueCategory: 7, vm.Venue: 20, + self.importer.import_venues(f) + self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 7, vm.Venue: 20, vm.VenueCategory.venues.through: 20}) # There are three bad lines in the CSV file, but each one generates # two errors: one creating the venue itself, and one creating the # venuecategory-venue relationship (because the venue doesn't exist). - self.assertEqual(len(errors), 6) + self.assertEqual(len(self.errors), 6) self.assertEqual(len(logscm.records), 6) self.importer.strict = True From 42321e1881e6d8814b0bfd63cba64239a0238044 Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 17:08:08 -0700 Subject: [PATCH 10/13] Second attempt to fix the tests #461 --- tabbycat/importer/tests/test_anorak.py | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tabbycat/importer/tests/test_anorak.py b/tabbycat/importer/tests/test_anorak.py index 9d33adbabd9..feee1a72755 100644 --- a/tabbycat/importer/tests/test_anorak.py +++ b/tabbycat/importer/tests/test_anorak.py @@ -50,15 +50,15 @@ def assertCountsDictEqual(self, counts, expected): # noqa def test_break_categories(self): f = self._open_csv_file(self.TESTDIR, "break_categories") self.importer.import_break_categories(f) - self.assertCountsDictEqual(self.counts, {bm.BreakCategory: 3}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {bm.BreakCategory: 3}) + self.assertFalse(self.importer.errors) def test_rounds(self): self.test_break_categories() f = self._open_csv_file(self.TESTDIR, "rounds") self.importer.import_rounds(f) - self.assertCountsDictEqual(self.counts, {tm.Round: 10}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {tm.Round: 10}) + self.assertFalse(self.importer.errors) def test_auto_make_rounds(self): self.importer.auto_make_rounds(7) @@ -67,77 +67,77 @@ def test_auto_make_rounds(self): def test_venues(self): f = self._open_csv_file(self.TESTDIR, "venues") self.importer.import_venues(f) - self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 8, vm.Venue: 23, + self.assertCountsDictEqual(self.importer.counts, {vm.VenueCategory: 8, vm.Venue: 23, vm.VenueCategory.venues.through: 22}) - self.assertFalse(self.errors) + self.assertFalse(self.importer.errors) def test_institutions(self): f = self._open_csv_file(self.TESTDIR, "institutions") self.importer.import_institutions(f) - self.assertCountsDictEqual(self.counts, {pm.Institution: 13, pm.Region: 6}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {pm.Institution: 13, pm.Region: 6}) + self.assertFalse(self.importer.errors) @skip("test file does not yet exist") def test_teams(self): f = self._open_csv_file(self.TESTDIR, "teams") # noqa self.importer.import_teams(self) - self.assertCountsDictEqual(self.counts, {pm.Team: 12}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {pm.Team: 12}) + self.assertFalse(self.importer.errors) def test_speakers(self): self.test_institutions() f = self._open_csv_file(self.TESTDIR, "speakers") self.importer.import_speakers(f) - self.assertCountsDictEqual(self.counts, {pm.Team: 24, pm.Speaker: 72}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {pm.Team: 24, pm.Speaker: 72}) + self.assertFalse(self.importer.errors) def test_adjudicators(self): self.test_speakers() f = self._open_csv_file(self.TESTDIR, "judges") self.importer.import_adjudicators(f) - self.assertCountsDictEqual(self.counts, { + self.assertCountsDictEqual(self.importer.counts, { pm.Adjudicator: 27, fm.AdjudicatorTestScoreHistory: 24, am.AdjudicatorInstitutionConflict: 36, am.AdjudicatorAdjudicatorConflict: 5, am.AdjudicatorConflict: 8, }) - self.assertFalse(self.errors) + self.assertFalse(self.importer.errors) def test_motions(self): self.test_rounds() f = self._open_csv_file(self.TESTDIR, "motions") self.importer.import_motions(f) - self.assertCountsDictEqual(self.counts, {mm.Motion: 18}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {mm.Motion: 18}) + self.assertFalse(self.importer.errors) def test_adj_feedback_questions(self): f = self._open_csv_file(self.TESTDIR, "questions") self.importer.import_adj_feedback_questions(f) - self.assertCountsDictEqual(self.counts, {fm.AdjudicatorFeedbackQuestion: 11}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {fm.AdjudicatorFeedbackQuestion: 11}) + self.assertFalse(self.importer.errors) def test_venue_categories(self): f = self._open_csv_file(self.TESTDIR, "venue_categories") self.importer.import_venue_categories(f) - self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 8}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {vm.VenueCategory: 8}) + self.assertFalse(self.importer.errors) def test_adj_venue_constraints(self): self.test_venue_categories() self.test_adjudicators() f = self._open_csv_file(self.TESTDIR, "adj_venue_constraints") self.importer.import_adj_venue_constraints(f) - self.assertCountsDictEqual(self.counts, {vm.VenueConstraint: 3}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {vm.VenueConstraint: 3}) + self.assertFalse(self.importer.errors) def test_team_venue_constraints(self): self.test_venue_categories() self.test_speakers() f = self._open_csv_file(self.TESTDIR, "team_venue_constraints") self.importer.import_team_venue_constraints(f) - self.assertCountsDictEqual(self.counts, {vm.VenueConstraint: 2}) - self.assertFalse(self.errors) + self.assertCountsDictEqual(self.importer.counts, {vm.VenueConstraint: 2}) + self.assertFalse(self.importer.errors) def test_invalid_line(self): self.test_speakers() @@ -152,14 +152,14 @@ def test_weird_choices_judges(self): self.test_speakers() f = self._open_csv_file(self.TESTDIR_CHOICES, "judges") self.importer.import_adjudicators(f) - self.assertCountsDictEqual(self.counts, { + self.assertCountsDictEqual(self.importer.counts, { pm.Adjudicator: 27, am.AdjudicatorAdjudicatorConflict: 0, fm.AdjudicatorTestScoreHistory: 27, am.AdjudicatorInstitutionConflict: 36, am.AdjudicatorConflict: 7, }) - self.assertFalse(self.errors) + self.assertFalse(self.importer.errors) def test_blank_entry_strict(self): f = self._open_csv_file(self.TESTDIR_ERRORS, "venues") @@ -177,11 +177,11 @@ def test_blank_entry_not_strict(self): self.importer.strict = False with self.assertLogs(self.logger, logging.WARNING) as logscm: self.importer.import_venues(f) - self.assertCountsDictEqual(self.counts, {vm.VenueCategory: 7, vm.Venue: 20, + self.assertCountsDictEqual(self.importer.counts, {vm.VenueCategory: 7, vm.Venue: 20, vm.VenueCategory.venues.through: 20}) # There are three bad lines in the CSV file, but each one generates # two errors: one creating the venue itself, and one creating the # venuecategory-venue relationship (because the venue doesn't exist). - self.assertEqual(len(self.errors), 6) + self.assertEqual(len(self.importer.errors), 6) self.assertEqual(len(logscm.records), 6) self.importer.strict = True From 1a7bd0b8bc8ea283d99dca04e1c4c9328749a12a Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 17:15:37 -0700 Subject: [PATCH 11/13] Reset counts explicitly after prerequisite tests #461 --- tabbycat/importer/tests/test_anorak.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tabbycat/importer/tests/test_anorak.py b/tabbycat/importer/tests/test_anorak.py index feee1a72755..65343b32625 100644 --- a/tabbycat/importer/tests/test_anorak.py +++ b/tabbycat/importer/tests/test_anorak.py @@ -55,6 +55,7 @@ def test_break_categories(self): def test_rounds(self): self.test_break_categories() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "rounds") self.importer.import_rounds(f) self.assertCountsDictEqual(self.importer.counts, {tm.Round: 10}) @@ -86,6 +87,7 @@ def test_teams(self): def test_speakers(self): self.test_institutions() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "speakers") self.importer.import_speakers(f) self.assertCountsDictEqual(self.importer.counts, {pm.Team: 24, pm.Speaker: 72}) @@ -93,6 +95,7 @@ def test_speakers(self): def test_adjudicators(self): self.test_speakers() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "judges") self.importer.import_adjudicators(f) self.assertCountsDictEqual(self.importer.counts, { @@ -106,6 +109,7 @@ def test_adjudicators(self): def test_motions(self): self.test_rounds() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "motions") self.importer.import_motions(f) self.assertCountsDictEqual(self.importer.counts, {mm.Motion: 18}) @@ -125,7 +129,9 @@ def test_venue_categories(self): def test_adj_venue_constraints(self): self.test_venue_categories() + self.importer.reset_counts() self.test_adjudicators() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "adj_venue_constraints") self.importer.import_adj_venue_constraints(f) self.assertCountsDictEqual(self.importer.counts, {vm.VenueConstraint: 3}) @@ -133,7 +139,9 @@ def test_adj_venue_constraints(self): def test_team_venue_constraints(self): self.test_venue_categories() + self.importer.reset_counts() self.test_speakers() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR, "team_venue_constraints") self.importer.import_team_venue_constraints(f) self.assertCountsDictEqual(self.importer.counts, {vm.VenueConstraint: 2}) @@ -141,6 +149,7 @@ def test_team_venue_constraints(self): def test_invalid_line(self): self.test_speakers() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR_ERRORS, "judges_invalid_line") with self.assertRaises(TournamentDataImporterError) as raisescm, self.assertLogs(self.logger, logging.ERROR) as logscm: self.importer.import_adjudicators(f) @@ -150,6 +159,7 @@ def test_invalid_line(self): def test_weird_choices_judges(self): self.test_speakers() + self.importer.reset_counts() f = self._open_csv_file(self.TESTDIR_CHOICES, "judges") self.importer.import_adjudicators(f) self.assertCountsDictEqual(self.importer.counts, { From e6587fd788c42d569bc434980caa5ef1f51a86db Mon Sep 17 00:00:00 2001 From: Chuan-Zheng Lee Date: Sun, 21 May 2017 17:38:57 -0700 Subject: [PATCH 12/13] Update CHANGELOG.rst --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bfb8de2586e..a95213ee44d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,13 @@ Change Log ========== +1.3.1 +----- +*Release date: TBD* + +- Fixed bug that allowed duplicate emoji to be occasionally generated + + 1.3.0 (Genetta) --------------- *Release date: 9 May 2017* From e19d10c1d15def3b5ca1bb16e6bd215eba34189a Mon Sep 17 00:00:00 2001 From: Philip Belesky Date: Fri, 26 May 2017 16:56:00 +1000 Subject: [PATCH 13/13] Bump version numbers for hotfix release --- CHANGELOG.rst | 2 +- docs/conf.py | 2 +- tabbycat/settings.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a95213ee44d..5353141f73b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Change Log 1.3.1 ----- -*Release date: TBD* +*Release date: 26 May 2017* - Fixed bug that allowed duplicate emoji to be occasionally generated diff --git a/docs/conf.py b/docs/conf.py index cfa73868c36..7ee93638880 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # The short X.Y version. version = '1.3' # The full version, including alpha/beta/rc tags. -release = '1.3.0' +release = '1.3.1' rst_epilog = """ .. |vrelease| replace:: v{release} diff --git a/tabbycat/settings.py b/tabbycat/settings.py index ba033903eee..3e5a09c2485 100644 --- a/tabbycat/settings.py +++ b/tabbycat/settings.py @@ -23,9 +23,9 @@ LANGUAGE_CODE = 'en' USE_I18N = True -TABBYCAT_VERSION = '1.3.0' +TABBYCAT_VERSION = '1.3.1' TABBYCAT_CODENAME = 'Genetta' -READTHEDOCS_VERSION = 'v1.3.0' +READTHEDOCS_VERSION = 'v1.3.1' LOCALE_PATHS = [ os.path.join(BASE_DIR, 'locale'),