Skip to content

Commit bebff3d

Browse files
reivvaxtwalen
authored andcommitted
Optimize queries for participants_data view (#533)
* Small performance changes * Add select_related() * Add some management tools * Remove timing utils * removing unused code * remove unused scripts --------- Co-authored-by: Tomasz Waleń <[email protected]>
1 parent 971f22e commit bebff3d

File tree

3 files changed

+167
-22
lines changed

3 files changed

+167
-22
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
import urllib.request
3+
import random
4+
5+
from datetime import date, timedelta
6+
7+
from django.contrib.auth.models import User
8+
from django.core.management.base import BaseCommand, CommandError
9+
from django.db import DatabaseError, transaction
10+
from django.utils.translation import gettext as _
11+
12+
from oioioi.contests.models import Contest
13+
from oioioi.participants.models import Participant
14+
from oioioi.oi.models import OIRegistration, School, T_SHIRT_SIZES, CLASS_TYPES
15+
16+
17+
class Command(BaseCommand):
18+
help = _(
19+
"Imports users and adds them as participants to <contest_id>.\n"
20+
"The users do not need to be in the database, they will be inserted dynamically.\n"
21+
"There should exist some School objects in database. If not, you can generate them with import_schools.py\n"
22+
"Each line must have: username first_name last_name (space or comma separated).\n"
23+
"Lines starting with '#' are ignored."
24+
)
25+
26+
def add_arguments(self, parser):
27+
parser.add_argument('contest_id', type=str, help='Contest to import to')
28+
parser.add_argument('filename_or_url', type=str, help='Source file')
29+
30+
def handle(self, *args, **options):
31+
try:
32+
contest = Contest.objects.get(id=options['contest_id'])
33+
except Contest.DoesNotExist:
34+
raise CommandError(_("Contest %s does not exist") % options['contest_id'])
35+
36+
arg = options['filename_or_url']
37+
if arg.startswith('http://') or arg.startswith('https://'):
38+
self.stdout.write(_("Fetching %s...\n") % (arg,))
39+
stream = urllib.request.urlopen(arg)
40+
stream = (line.decode('utf-8') for line in stream)
41+
else:
42+
if not os.path.exists(arg):
43+
raise CommandError(_("File not found: %s") % arg)
44+
stream = open(arg, 'r', encoding='utf-8')
45+
46+
schools = list(School.objects.all())
47+
if not schools:
48+
raise CommandError("No schools found in the database.")
49+
50+
all_count = 0
51+
with transaction.atomic():
52+
ok = True
53+
for line in stream:
54+
line = line.strip()
55+
if not line or line.startswith('#'):
56+
continue
57+
58+
parts = line.replace(',', ' ').split()
59+
if len(parts) != 3:
60+
self.stdout.write(_("Invalid line format: %s\n") % line)
61+
ok = False
62+
continue
63+
64+
username, first_name, last_name = parts
65+
66+
try:
67+
user, created = User.objects.get_or_create(username=username)
68+
if created:
69+
user.first_name = first_name
70+
user.last_name = last_name
71+
user.set_unusable_password()
72+
user.save()
73+
74+
Participant.objects.get_or_create(contest=contest, user=user)
75+
participant, _ = Participant.objects.get_or_create(contest=contest, user=user)
76+
77+
OIRegistration.objects.create(
78+
participant=participant,
79+
address=f"ulica {random.randint(1, 100)}",
80+
postal_code=f"{random.randint(10, 99)}-{random.randint(100, 999)}",
81+
city=f"Miasto{random.randint(1, 50)}",
82+
phone=f"+48 123 456 {random.randint(100, 999)}",
83+
birthday=date.today() - timedelta(days=random.randint(5000, 8000)),
84+
birthplace=f"Miejsce{random.randint(1, 100)}",
85+
t_shirt_size=random.choice(T_SHIRT_SIZES)[0],
86+
class_type=random.choice(CLASS_TYPES)[0],
87+
school=random.choice(schools),
88+
terms_accepted=True,
89+
)
90+
all_count += 1
91+
except DatabaseError as e:
92+
self.stdout.write(_("DB Error for user=%s: %s\n") % (username, str(e)))
93+
ok = False
94+
95+
if ok:
96+
print(f"Successfully processed {all_count} entries.")
97+
else:
98+
raise CommandError(_("There were some errors. Database not changed."))

oioioi/participants/utils.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from django.contrib.auth.models import User
33
from django.http import HttpResponse
44
from django.utils.encoding import force_str
5+
from django.core.exceptions import ObjectDoesNotExist
6+
from django.db.models.fields.related import ForeignKey, OneToOneField
7+
8+
from collections import deque
59

610
from oioioi.base.permissions import make_request_condition
711
from oioioi.base.utils import request_cached
@@ -90,7 +94,8 @@ def _fold_registration_models_tree(object):
9094
the object, gets models related to the model and lists
9195
all their fields."""
9296
result = []
93-
objects_used = [object]
97+
objects_used = set()
98+
objects_used.add(object)
9499

95100
# https://docs.djangoproject.com/en/1.9/ref/models/meta/#migrating-old-meta-api
96101
def get_all_related_objects(_meta):
@@ -100,16 +105,16 @@ def get_all_related_objects(_meta):
100105
if (f.one_to_many or f.one_to_one) and f.auto_created and not f.concrete
101106
]
102107

103-
objs = [
104-
getattr(object, rel.get_accessor_name())
105-
for rel in get_all_related_objects(object._meta)
106-
if hasattr(object, rel.get_accessor_name())
107-
]
108+
objs = deque()
109+
for rel in get_all_related_objects(object._meta):
110+
if hasattr(object, rel.get_accessor_name()):
111+
objs.append(getattr(object, rel.get_accessor_name()))
112+
108113
while objs:
109-
current = objs.pop(0)
114+
current = objs.popleft()
110115
if current is None:
111116
continue
112-
objects_used.append(current)
117+
objects_used.add(current)
113118

114119
for field in current._meta.fields:
115120
if (
@@ -123,17 +128,59 @@ def get_all_related_objects(_meta):
123128
if not field.auto_created:
124129
if field.remote_field is None:
125130
result += [(obj, field)]
131+
126132
return result
127133

128134

129-
def serialize_participants_data(request, participants):
135+
def get_related_paths(model, prefix='', depth=5, visited=None):
136+
if visited is None:
137+
visited = set()
138+
if model in visited or depth == 0:
139+
return []
140+
141+
visited.add(model)
142+
paths = []
143+
try:
144+
for field in model._meta.get_fields():
145+
if isinstance(field, (ForeignKey, OneToOneField)) and not field.auto_created:
146+
related_model = field.related_model
147+
if related_model == Participant:
148+
continue # skip backward pointer to Participant
149+
150+
full_path = f"{prefix}__{field.name}" if prefix else field.name
151+
paths.append(full_path)
152+
153+
paths.extend(
154+
get_related_paths(related_model, prefix=full_path, depth=depth - 1, visited=visited)
155+
)
156+
finally:
157+
visited.remove(model)
158+
159+
return paths
160+
161+
162+
def serialize_participants_data(request):
130163
"""Serializes all personal data of participants to a table.
131-
:param participants: A QuerySet from table participants.
132164
"""
133-
134-
if not participants.exists():
165+
participant = Participant.objects.filter(contest=request.contest).first()
166+
if participant is None:
135167
return {'no_participants': True}
136168

169+
try: # Check if registration model exists
170+
registration_model_instance = participant.registration_model
171+
registration_model_class = registration_model_instance.__class__
172+
registration_model_name = registration_model_instance._meta.get_field('participant').remote_field.related_name
173+
174+
related = get_related_paths(registration_model_class, prefix=registration_model_name, depth=10)
175+
related.extend(['user', 'contest', registration_model_name])
176+
participants = (
177+
Participant.objects
178+
.filter(contest=request.contest)
179+
.select_related(*related)
180+
)
181+
except ObjectDoesNotExist: # It doesn't, so no need to select anything
182+
participants = Participant.objects.filter(contest=request.contest)
183+
137184
display_email = request.contest.controller.show_email_in_participants_data
138185

139186
keys = ['username', 'user ID', 'first name', 'last name'] + (
@@ -144,9 +191,11 @@ def key_name(attr):
144191
(obj, field) = attr
145192
return str(obj.__class__.__name__) + ": " + field.verbose_name.title()
146193

194+
folded_participants = [(participant, _fold_registration_models_tree(participant)) for participant in participants]
195+
147196
set_of_keys = set(keys)
148-
for participant in participants:
149-
for key in map(key_name, _fold_registration_models_tree(participant)):
197+
for participant, folded in folded_participants:
198+
for key in map(key_name, folded):
150199
if key not in set_of_keys:
151200
set_of_keys.add(key)
152201
keys.append(key)
@@ -156,8 +205,8 @@ def key_value(attr):
156205
return (key_name((obj, field)), field.value_to_string(obj))
157206

158207
data = []
159-
for participant in participants:
160-
values = dict(list(map(key_value, _fold_registration_models_tree(participant))))
208+
for participant, folded in folded_participants:
209+
values = dict(list(map(key_value, folded)))
161210
values['username'] = participant.user.username
162211
values['user ID'] = participant.user.id
163212
values['first name'] = participant.user.first_name
@@ -169,8 +218,8 @@ def key_value(attr):
169218
return {'keys': keys, 'data': data}
170219

171220

172-
def render_participants_data_csv(request, participants, name):
173-
data = serialize_participants_data(request, participants)
221+
def render_participants_data_csv(request, name):
222+
data = serialize_participants_data(request)
174223
response = HttpResponse(content_type='text/csv')
175224
response['Content-Disposition'] = 'attachment; filename=%s-%s.csv' % (
176225
name,

oioioi/participants/views.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ def unregistration_view(request):
8484
not_anonymous & contest_exists & contest_has_participants & can_see_personal_data
8585
)
8686
def participants_data(request):
87-
context = serialize_participants_data(
88-
request, Participant.objects.filter(contest=request.contest)
89-
)
87+
context = serialize_participants_data(request)
9088
return TemplateResponse(request, 'participants/data.html', context)
9189

9290

@@ -95,5 +93,5 @@ def participants_data(request):
9593
)
9694
def participants_data_csv(request):
9795
return render_participants_data_csv(
98-
request, Participant.objects.filter(contest=request.contest), request.contest.id
96+
request, request.contest.id
9997
)

0 commit comments

Comments
 (0)