Skip to content

Conversation

@JoeyRubas
Copy link
Member

@JoeyRubas JoeyRubas commented Nov 28, 2024

Impliment CSV, JSON exports for tab card data. Some refactors of existing tab card gen

Copy link
Member

@BenMusch BenMusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR! A couple comments in line. It's been a while since I've written python or django code so take any of my comments and examples with a grain of salt

The last time I ran this code was 1-2 laptops ago so will take me a day or two to set things up to test the behavior here

(Also: the code in basically any module here is pretty outdated and hack-y, so apologies if any of my review of this PR is me nit-picking code that was just copied over. Would love to take this as an opportunity to clean things up but feel free to let me know if/where I've left comments on code that's just being copied and we can de-prioritize those changes)

all_tab_cards_data = {}

# Prefetch related data to reduce database queries
teams = Team.objects.prefetch_related(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we wanna fetch judges here as well, lemme test locally to see how to do that

@JoeyRubas
Copy link
Member Author

Resolved comments that were added, rest should have comment replies and usually an associated change

@BenMusch
Copy link
Member

@JoeyRubas Can you fix the linting errors in circle CI? After that will run manually to confirm it works and then merge

************* Module mittab.apps.tab.models
mittab/apps/tab/models.py:278:0: C0303: Trailing whitespace (trailing-whitespace)
************* Module mittab.apps.tab.team_views
mittab/apps/tab/team_views.py:8:0: C0301: Line too long (116/88) (line-too-long)
mittab/apps/tab/team_views.py:235:0: C0301: Line too long (98/88) (line-too-long)
mittab/apps/tab/team_views.py:235:77: C0326: No space allowed around keyword argument assignment
    json_data = json.dumps({"tab_cards": get_all_json_data()}, indent=4, cls = JSONDecimalEncoder)
                                                                             ^ (bad-whitespace)
mittab/apps/tab/team_views.py:245:33: C0303: Trailing whitespace (trailing-whitespace)
mittab/apps/tab/team_views.py:239:13: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/apps/tab/team_views.py:239:38: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/apps/tab/team_views.py:243:41: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/apps/tab/team_views.py:244:13: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/apps/tab/team_views.py:244:38: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/apps/tab/team_views.py:16:0: W0404: Reimport 'HttpResponse' (imported line 3) (reimported)
mittab/apps/tab/team_views.py:16:0: C0411: third party import "from django.http import HttpResponse as HTTPResponse" should be placed before "from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm" (wrong-import-order)
mittab/apps/tab/team_views.py:16:0: C0412: Imports from package django are not grouped (ungrouped-imports)
************* Module mittab.libs.data_import.tab_card
mittab/libs/data_import/tab_card.py:8:0: C0301: Line too long (92/88) (line-too-long)
mittab/libs/data_import/tab_card.py:27:0: C0301: Line too long (108/88) (line-too-long)
mittab/libs/data_import/tab_card.py:27:0: C0301: Line too long (108/88) (line-too-long)
mittab/libs/data_import/tab_card.py:29:0: C0330: Wrong hanging indentation (remove 4 spaces).
            k for k in RoundStats.objects.filter(debater=deb1).filter(
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:58:0: C0301: Line too long (97/88) (line-too-long)
mittab/libs/data_import/tab_card.py:62:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:70:0: C0301: Line too long (90/88) (line-too-long)
mittab/libs/data_import/tab_card.py:74:33: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:78:33: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:95:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:98:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:110:0: C0301: Line too long (104/88) (line-too-long)
mittab/libs/data_import/tab_card.py:110:24: C0326: No space allowed after bracket
        tab_card_data = { "team_name": team.get_or_create_team_code(), "team_school": team.school.name }
                        ^ (bad-whitespace)
mittab/libs/data_import/tab_card.py:110:103: C0326: No space allowed before bracket
        tab_card_data = { "team_name": team.get_or_create_team_code(), "team_school": team.school.name }
                                                                                                       ^ (bad-whitespace)
mittab/libs/data_import/tab_card.py:116:0: C0301: Line too long (89/88) (line-too-long)
mittab/libs/data_import/tab_card.py:117:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:121:0: C0301: Line too long (93/88) (line-too-long)
mittab/libs/data_import/tab_card.py:133:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:142:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:144:0: C0301: Line too long (92/88) (line-too-long)
mittab/libs/data_import/tab_card.py:167:0: C0301: Line too long (95/88) (line-too-long)
mittab/libs/data_import/tab_card.py:171:0: C0330: Wrong continued indentation (add 21 spaces).
                                get_victor_label(round_obj.victor, side),
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:172:0: C0330: Wrong continued indentation (add 21 spaces).
                                opponent.display_backend,
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:173:0: C0330: Wrong continued indentation (add 21 spaces).
                                " - ".join(j.name for j in round_obj.judges.all()),
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:174:0: C0330: Wrong continued indentation (add 21 spaces).
                                (float(dstat1.speaks), float(dstat1.ranks)),
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:175:0: C0330: Wrong continued indentation (add 21 spaces).
                                (float(dstat2.speaks), float(dstat2.ranks)),
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:176:0: C0330: Wrong continued indentation (add 21 spaces).
                                (float(speaksRolling), float(ranksRolling))]
                                ^                    | (bad-continuation)
mittab/libs/data_import/tab_card.py:177:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:179:0: C0301: Line too long (122/88) (line-too-long)
mittab/libs/data_import/tab_card.py:188:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "team_school": team.school,
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:189:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "debater_1": deb1.name,
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:190:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "debater_1_status": Debater.NOVICE_CHOICES[deb1.novice_status][1],
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:191:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "debater_2": deb2.name,
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:192:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "debater_2_status": Debater.NOVICE_CHOICES[deb2.novice_status][1],
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:193:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "round_stats": round_stats,
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:194:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "d1st": tot_speaks_deb(deb1),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:195:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "d1rt": tot_ranks_deb(deb1),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:196:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "d2st": tot_speaks_deb(deb2),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:197:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "d2rt": tot_ranks_deb(deb2),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:198:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "ts": tot_speaks(team),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:199:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "tr": tot_ranks(team),
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:200:0: C0330: Wrong hanging indentation (remove 4 spaces).
            "bye_round": bye_round
        |   ^ (bad-continuation)
mittab/libs/data_import/tab_card.py:206:62: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:207:0: C0301: Line too long (147/88) (line-too-long)
mittab/libs/data_import/tab_card.py:212:17: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:213:19: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:214:19: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:233:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:237:0: C0301: Line too long (95/88) (line-too-long)
mittab/libs/data_import/tab_card.py:240:0: C0301: Line too long (103/88) (line-too-long)
mittab/libs/data_import/tab_card.py:241:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:242:82: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:246:23: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:247:30: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:249:22: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:250:26: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:251:28: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:252:54: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:259:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:263:64: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:268:0: C0301: Line too long (143/88) (line-too-long)
mittab/libs/data_import/tab_card.py:270:0: C0303: Trailing whitespace (trailing-whitespace)
mittab/libs/data_import/tab_card.py:291:0: C0304: Final newline missing (missing-final-newline)
mittab/libs/data_import/tab_card.py:101:8: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:102:17: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:103:17: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:104:17: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:212:8: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:213:8: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:214:8: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:215:8: C4001: Invalid string quote ', should be " (invalid-string-quote)
mittab/libs/data_import/tab_card.py:5:0: W0404: Reimport 'Bye' (imported line 4) (reimported)
mittab/libs/data_import/tab_card.py:5:0: W0404: Reimport 'RoundStats' (imported line 4) (reimported)
mittab/libs/data_import/tab_card.py:84:8: W0104: Statement seems to have no effect (pointless-statement)
mittab/libs/data_import/tab_card.py:91:4: W0221: Parameters differ from overridden 'default' method (arguments-differ)
mittab/libs/data_import/tab_card.py:91:4: E0202: An attribute defined in json.encoder line 158 hides this method (method-hidden)
mittab/libs/data_import/tab_card.py:107:4: W0612: Unused variable 'total_rounds' (unused-variable)
mittab/libs/data_import/tab_card.py:159:4: C0103: Variable name "speaksRolling" doesn't conform to snake_case naming style (invalid-name)
mittab/libs/data_import/tab_card.py:160:4: C0103: Variable name "ranksRolling" doesn't conform to snake_case naming style (invalid-name)
mittab/libs/data_import/tab_card.py:168:12: C0103: Variable name "speaksRolling" doesn't conform to snake_case naming style (invalid-name)
mittab/libs/data_import/tab_card.py:169:12: C0103: Variable name "ranksRolling" doesn't conform to snake_case naming style (invalid-name)
mittab/libs/data_import/tab_card.py:6:0: C0411: third party import "from django.db.models import Prefetch" should be placed before "from mittab.apps.tab.helpers import redirect_and_flash_error" (wrong-import-order)
mittab/libs/data_import/tab_card.py:8:0: C0412: Imports from package mittab are not grouped (ungrouped-imports)

@JoeyRubas
Copy link
Member Author

JoeyRubas commented Jan 18, 2025

@BenMusch Resolved the linting errors and in extension of this discussion, moved all three exports to one url, and a dedicated page/form to prevent navbar clutter. Everything works well by my testing, should be ready for testing by others! Also going to flag one or two other design choices that might require discussion below.

Edit: has to fix one more linting error, now resolved.

@permission_required("tab.tab_settings.can_change", login_url="/403/")
def generate_archive(request):
tournament_name = request.META["SERVER_NAME"].split(".")[0]
def export_tournament(request):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I migrated the data export views from team_views info views, because I wanted them in the same file as the existing XML/archive, and this seemed more natural (broad tournament data doesn't seem too tightly coupled with teams?). The drawback is they're now in a different file than the HTML tab cards.



class JSONDecimalEncoder(json.JSONEncoder):
def default(self, o): # pylint: disable=E0202
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per this issue on the pylint github, it seems like the way json wants you to handle setting up this encoder is incompatable with pylint's preferred course of action - hence the comment

@JoeyRubas JoeyRubas changed the title Impliment CSV, JSON exports for tab cards. Refactor existing tab card… Tournament Data Export + Tab Card Refactor (Closes #314) Jan 19, 2025
@JoeyRubas JoeyRubas changed the title Tournament Data Export + Tab Card Refactor (Closes #314) Tournament Data Export + Tab Card Refactor (Closes #341) Jan 19, 2025
@JoeyRubas JoeyRubas changed the title Tournament Data Export + Tab Card Refactor (Closes #341) Tournament Data Export + Tab Card Refactor (Closes MIT-Tab/mit-tab#341) Jan 19, 2025
@JoeyRubas JoeyRubas changed the title Tournament Data Export + Tab Card Refactor (Closes MIT-Tab/mit-tab#341) Tournament Data Export + Tab Card Refactor Jan 19, 2025
@JoeyRubas JoeyRubas marked this pull request as draft January 19, 2025 16:13
@JoeyRubas JoeyRubas marked this pull request as ready for review January 19, 2025 19:35
Copy link
Member

@BenMusch BenMusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some minor edge cases, I think this could use 1 more pass

@JoeyRubas JoeyRubas force-pushed the master branch 2 times, most recently from b9653d1 to 29c52e5 Compare October 26, 2025 16:59
@JoeyRubas JoeyRubas requested a review from Copilot November 10, 2025 18:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new tournament export feature that allows administrators to download tournament data in JSON, CSV, and XML formats. The implementation refactors existing export logic into reusable modules and adds a user-friendly interface for selecting export formats.

  • Adds a new export tournament interface with support for JSON, CSV, and XML formats
  • Refactors tab card and XML archive generation into dedicated modules (mittab/libs/data_export/)
  • Enhances storage abstraction to support configurable prefixes and suffixes for different export types
  • Integrates automatic export when results are published

Reviewed Changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
mittab/urls.py Adds URL patterns for the new export tournament feature
mittab/templates/common/_form.html Adds support for custom submit button labels
mittab/templates/base/_navigation.html Adds "Export Tournament" link to navigation menu
mittab/libs/tests/views/test_exports.py Comprehensive test coverage for all export formats including edge cases
mittab/libs/data_export/xml_archive.py New module for XML (DebateXML) tournament archive export
mittab/libs/data_export/tab_card.py New module for JSON/CSV tab card exports with optimized queries
mittab/libs/data_export/s3_connector.py New module for automated results export to storage
mittab/libs/backup/storage.py Refactors storage classes to accept configurable prefix/suffix parameters
mittab/apps/tab/views/views.py Adds export views and refactors existing archive generation
mittab/apps/tab/views/team_views.py Simplifies tab_card view by using extracted export logic
mittab/apps/tab/models.py Adds helper method to get or create team codes
mittab/apps/tab/forms.py Adds ExportFormatForm for format selection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

def get_victor_label(victor_code, side):
side = 0 if side == GOV else 1
victor_map = {
0 : ("", ""),
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing in dictionary. There's a missing space after the colon on line 59 (0 : ("", "")) while other entries have proper spacing. Change to 0: ("", "") for consistency.

Suggested change
0 : ("", ""),
0: ("", ""),

Copilot uses AI. Check for mistakes.
breaking_team.effective_seed = breaking_team.seed
breaking_team.save()

return round_obj
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line before class definition. According to PEP 8, there should be two blank lines before top-level class definitions. Add a blank line before class ExportFormatForm.

Suggested change
return round_obj
return round_obj

Copilot uses AI. Check for mistakes.
Comment on lines +511 to +512
def _get_tournament_name(request):
return request.META["SERVER_NAME"].split(".")[0]
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential security issue: The tournament name is extracted from SERVER_NAME without validation and used directly in the Content-Disposition header. A malicious SERVER_NAME could potentially inject headers. Consider sanitizing the tournament name or using a safer approach to generate filenames, such as using only alphanumeric characters and hyphens.

Copilot uses AI. Check for mistakes.
def __init__(self, prefix=BACKUP_PREFIX, suffix=SUFFIX):
if not BUCKET_NAME:
raise ValueError("Need bucket name for S3 storage")
if not BACKUP_PREFIX:
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation still checks the global BACKUP_PREFIX constant instead of the instance's prefix parameter. This means validation will fail even when a valid prefix parameter is passed. Line 56 should check if not prefix: or if not self.prefix: instead of if not BACKUP_PREFIX:.

Suggested change
if not BACKUP_PREFIX:
if not prefix:

Copilot uses AI. Check for mistakes.
max_index = min(cur_round - 1, len(round_stats))
for i in range(max_index):
# Don't fill in totals for incomplete rounds
if round_stats[i][6] == blank and len(round_stats)>=i+2 and blank not in round_stats[i + 1][:5]:
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space before the comparison operator. Should be len(round_stats) >= i+2 with spacing: len(round_stats) >= i + 2 for better readability and consistency with Python style guidelines.

Suggested change
if round_stats[i][6] == blank and len(round_stats)>=i+2 and blank not in round_stats[i + 1][:5]:
if round_stats[i][6] == blank and len(round_stats) >= i + 2 and blank not in round_stats[i + 1][:5]:

Copilot uses AI. Check for mistakes.
def tab_cards_csv(request, tournament_name):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={tournament_name}.csv"
buffer = io.StringIO(newline="")
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The io.StringIO is created with newline="" parameter which is not supported. The StringIO constructor doesn't accept a newline parameter - that's only for opening files. Remove the newline="" argument.

Suggested change
buffer = io.StringIO(newline="")
buffer = io.StringIO()

Copilot uses AI. Check for mistakes.
JoeyRubas and others added 4 commits November 10, 2025 13:15
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants