1
- import datetime
2
1
from typing import List
3
2
4
3
from flask import abort , render_template , request , url_for
5
4
from flask_restx import Namespace , Resource
6
- from sqlalchemy import func as sa_func
7
- from sqlalchemy .sql import and_ , false , true
5
+ from sqlalchemy .sql import and_
8
6
9
7
from CTFd .api .v1 .helpers .request import validate_args
10
8
from CTFd .api .v1 .helpers .schemas import sqlalchemy_to_pydantic
11
9
from CTFd .api .v1 .schemas import APIDetailedSuccessResponse , APIListSuccessResponse
12
- from CTFd .cache import clear_standings
10
+ from CTFd .cache import clear_challenges , clear_standings
13
11
from CTFd .constants import RawEnum
14
12
from CTFd .models import ChallengeFiles as ChallengeFilesModel
15
13
from CTFd .models import Challenges
22
20
from CTFd .schemas .tags import TagSchema
23
21
from CTFd .utils import config , get_config
24
22
from CTFd .utils import user as current_user
23
+ from CTFd .utils .challenges import (
24
+ get_all_challenges ,
25
+ get_solve_counts_for_challenges ,
26
+ get_solve_ids_for_user_id ,
27
+ get_solves_for_challenge_id ,
28
+ )
25
29
from CTFd .utils .config .visibility import (
26
30
accounts_visible ,
27
31
challenges_visible ,
28
32
scores_visible ,
29
33
)
30
- from CTFd .utils .dates import ctf_ended , ctf_paused , ctftime , isoformat , unix_time_to_utc
34
+ from CTFd .utils .dates import ctf_ended , ctf_paused , ctftime
31
35
from CTFd .utils .decorators import (
32
36
admins_only ,
33
37
during_ctf_time_only ,
37
41
check_challenge_visibility ,
38
42
check_score_visibility ,
39
43
)
40
- from CTFd .utils .helpers .models import build_model_filters
41
44
from CTFd .utils .logging import log
42
- from CTFd .utils .modes import generate_account_url , get_model
43
45
from CTFd .utils .security .signing import serialize
44
46
from CTFd .utils .user import (
45
47
authed ,
@@ -77,60 +79,6 @@ class ChallengeListSuccessResponse(APIListSuccessResponse):
77
79
)
78
80
79
81
80
- def _build_solves_query (extra_filters = (), admin_view = False ):
81
- """Returns queries and data that that are used for showing an account's solves.
82
- It returns a tuple of
83
- - SQLAlchemy query with (challenge_id, solve_count_for_challenge_id)
84
- - Current user's solved challenge IDs
85
- """
86
- # This can return None (unauth) if visibility is set to public
87
- user = get_current_user ()
88
- # We only set a condition for matching user solves if there is a user and
89
- # they have an account ID (user mode or in a team in teams mode)
90
- AccountModel = get_model ()
91
- if user is not None and user .account_id is not None :
92
- user_solved_cond = Solves .account_id == user .account_id
93
- else :
94
- user_solved_cond = false ()
95
- # We have to filter solves to exclude any made after the current freeze
96
- # time unless we're in an admin view as determined by the caller.
97
- freeze = get_config ("freeze" )
98
- if freeze and not admin_view :
99
- freeze_cond = Solves .date < unix_time_to_utc (freeze )
100
- else :
101
- freeze_cond = true ()
102
- # Finally, we never count solves made by hidden or banned users/teams, even
103
- # if we are an admin. This is to match the challenge detail API.
104
- exclude_solves_cond = and_ (
105
- AccountModel .banned == false (), AccountModel .hidden == false (),
106
- )
107
- # This query counts the number of solves per challenge, as well as the sum
108
- # of correct solves made by the current user per the condition above (which
109
- # should probably only be 0 or 1!)
110
- solves_q = (
111
- db .session .query (Solves .challenge_id , sa_func .count (Solves .challenge_id ),)
112
- .join (AccountModel )
113
- .filter (* extra_filters , freeze_cond , exclude_solves_cond )
114
- .group_by (Solves .challenge_id )
115
- )
116
- # Also gather the user's solve items which can be different from above query
117
- # For example, even if we are a hidden user, we should see that we have solved a challenge
118
- # however as a hidden user we are not included in the count of the above query
119
- if admin_view :
120
- # If we're an admin we should show all challenges as solved to break through any requirements
121
- challenges = Challenges .query .all ()
122
- solve_ids = {challenge .id for challenge in challenges }
123
- else :
124
- # If not an admin we calculate solves as normal
125
- solve_ids = (
126
- Solves .query .with_entities (Solves .challenge_id )
127
- .filter (user_solved_cond )
128
- .all ()
129
- )
130
- solve_ids = {value for value , in solve_ids }
131
- return solves_q , solve_ids
132
-
133
-
134
82
@challenges_namespace .route ("" )
135
83
class ChallengeList (Resource ):
136
84
@check_challenge_visibility
@@ -185,23 +133,22 @@ def get(self, query_args):
185
133
# Build filtering queries
186
134
q = query_args .pop ("q" , None )
187
135
field = str (query_args .pop ("field" , None ))
188
- filters = build_model_filters (model = Challenges , query = q , field = field )
189
136
190
137
# Admins get a shortcut to see all challenges despite pre-requisites
191
138
admin_view = is_admin () and request .args .get ("view" ) == "admin"
192
139
193
- solve_counts = {}
194
- # Build a query for to show challenge solve information. We only
195
- # give an admin view if the request argument has been provided.
196
- #
197
- # NOTE: This is different behaviour to the challenge detail
198
- # endpoint which only needs the current user to be an admin rather
199
- # than also also having to provide `view=admin` as a query arg.
200
- solves_q , user_solves = _build_solves_query (admin_view = admin_view )
140
+ # Get a cached mapping of challenge_id to solve_count
141
+ solve_counts = get_solve_counts_for_challenges (admin = admin_view )
142
+
143
+ # Get list of solve_ids for current user
144
+ if authed ():
145
+ user = get_current_user ()
146
+ user_solves = get_solve_ids_for_user_id (user_id = user .id )
147
+ else :
148
+ user_solves = set ()
149
+
201
150
# Aggregate the query results into the hashes defined at the top of
202
151
# this block for later use
203
- for chal_id , solve_count in solves_q :
204
- solve_counts [chal_id ] = solve_count
205
152
if scores_visible () and accounts_visible ():
206
153
solve_count_dfl = 0
207
154
else :
@@ -211,18 +158,7 @@ def get(self, query_args):
211
158
# `None` for the solve count if visiblity checks fail
212
159
solve_count_dfl = None
213
160
214
- # Build the query for the challenges which may be listed
215
- chal_q = Challenges .query
216
- # Admins can see hidden and locked challenges in the admin view
217
- if admin_view is False :
218
- chal_q = chal_q .filter (
219
- and_ (Challenges .state != "hidden" , Challenges .state != "locked" )
220
- )
221
- chal_q = (
222
- chal_q .filter_by (** query_args )
223
- .filter (* filters )
224
- .order_by (Challenges .value , Challenges .id )
225
- )
161
+ chal_q = get_all_challenges (admin = admin_view , field = field , q = q , ** query_args )
226
162
227
163
# Iterate through the list of challenges, adding to the object which
228
164
# will be JSONified back to the client
@@ -308,6 +244,9 @@ def post(self):
308
244
challenge_class = get_chal_class (challenge_type )
309
245
challenge = challenge_class .create (request )
310
246
response = challenge_class .read (challenge )
247
+
248
+ clear_challenges ()
249
+
311
250
return {"success" : True , "data" : response }
312
251
313
252
@@ -453,13 +392,17 @@ def get(self, challenge_id):
453
392
454
393
response = chal_class .read (challenge = chal )
455
394
456
- solves_q , user_solves = _build_solves_query (
457
- extra_filters = (Solves .challenge_id == chal .id ,)
458
- )
459
- # If there are no solves for this challenge ID then we have 0 rows
460
- maybe_row = solves_q .first ()
461
- if maybe_row :
462
- challenge_id , solve_count = maybe_row
395
+ # Get list of solve_ids for current user
396
+ if authed ():
397
+ user = get_current_user ()
398
+ user_solves = get_solve_ids_for_user_id (user_id = user .id )
399
+ else :
400
+ user_solves = []
401
+
402
+ solves_count = get_solve_counts_for_challenges (challenge_id = chal .id )
403
+ if solves_count :
404
+ challenge_id = chal .id
405
+ solve_count = solves_count .get (chal .id )
463
406
solved_by_user = challenge_id in user_solves
464
407
else :
465
408
solve_count , solved_by_user = 0 , False
@@ -522,6 +465,10 @@ def patch(self, challenge_id):
522
465
challenge_class = get_chal_class (challenge .type )
523
466
challenge = challenge_class .update (challenge , request )
524
467
response = challenge_class .read (challenge )
468
+
469
+ clear_standings ()
470
+ clear_challenges ()
471
+
525
472
return {"success" : True , "data" : response }
526
473
527
474
@admins_only
@@ -534,6 +481,9 @@ def delete(self, challenge_id):
534
481
chal_class = get_chal_class (challenge .type )
535
482
chal_class .delete (challenge )
536
483
484
+ clear_standings ()
485
+ clear_challenges ()
486
+
537
487
return {"success" : True }
538
488
539
489
@@ -675,6 +625,7 @@ def post(self):
675
625
user = user , team = team , challenge = challenge , request = request
676
626
)
677
627
clear_standings ()
628
+ clear_challenges ()
678
629
679
630
log (
680
631
"submissions" ,
@@ -694,6 +645,7 @@ def post(self):
694
645
user = user , team = team , challenge = challenge , request = request
695
646
)
696
647
clear_standings ()
648
+ clear_challenges ()
697
649
698
650
log (
699
651
"submissions" ,
@@ -762,41 +714,15 @@ def get(self, challenge_id):
762
714
if challenge .state == "hidden" and is_admin () is False :
763
715
abort (404 )
764
716
765
- Model = get_model ()
766
-
767
- # Note that we specifically query for the Solves.account.name
768
- # attribute here because it is faster than having SQLAlchemy
769
- # query for the attribute directly and it's unknown what the
770
- # affects of changing the relationship lazy attribute would be
771
- solves = (
772
- Solves .query .add_columns (Model .name .label ("account_name" ))
773
- .join (Model , Solves .account_id == Model .id )
774
- .filter (
775
- Solves .challenge_id == challenge_id ,
776
- Model .banned == False ,
777
- Model .hidden == False ,
778
- )
779
- .order_by (Solves .date .asc ())
780
- )
781
-
782
717
freeze = get_config ("freeze" )
783
718
if freeze :
784
719
preview = request .args .get ("preview" )
785
720
if (is_admin () is False ) or (is_admin () is True and preview ):
786
- dt = datetime .datetime .utcfromtimestamp (freeze )
787
- solves = solves .filter (Solves .date < dt )
721
+ freeze = True
722
+ elif is_admin () is True :
723
+ freeze = False
788
724
789
- for solve in solves :
790
- # Seperate out the account name and the Solve object from the SQLAlchemy tuple
791
- solve , account_name = solve
792
- response .append (
793
- {
794
- "account_id" : solve .account_id ,
795
- "name" : account_name ,
796
- "date" : isoformat (solve .date ),
797
- "account_url" : generate_account_url (account_id = solve .account_id ),
798
- }
799
- )
725
+ response = get_solves_for_challenge_id (challenge_id = challenge_id , freeze = freeze )
800
726
801
727
return {"success" : True , "data" : response }
802
728
0 commit comments