@@ -190,6 +190,65 @@ def get_queryset(self, request):
190190 return self .scope_view (request , queryset )
191191
192192
193+ def get_columns (self , request ):
194+ fields , annotations , properties = self .scope_columns (request )
195+
196+ if fields is not None :
197+ fields = list (map (self .model ._meta .get_field , fields ))
198+
199+ return fields , annotations , properties
200+
201+
202+ def scope_columns (self , request ):
203+ """
204+ Each view scope may optionally declare which columns (fields, annotations, properties)
205+ ought to be exposed to the user. So view scope functions may return a tuple of (rows, columns)
206+ instead of rows only. Columns are specified like so:
207+ {
208+ 'fields': ['id', 'name', ...] | None,
209+ 'annotations': ['derived_name', 'bsn', ...] | None,
210+ 'properties': ['amount', ...] | None,
211+ }
212+ Where 'None' means, that no scoping is being performed on that column type.
213+ If multiple functions with scoped columns exist, we take the set union.
214+ """
215+
216+ # helper function to take the set union of columns
217+ def append_columns (columns , new_columns ):
218+ if new_columns is None :
219+ return columns
220+ if columns is None :
221+ columns = set ()
222+ return columns | set (new_columns )
223+
224+ scopes = self ._require_model_perm ('view' , request )
225+
226+ fields = None # this is equivalent to all fields
227+ annotations = None # this is equivalent to all annotations
228+ properties = None # this is equivalent to all properties
229+
230+ for s in scopes :
231+ scope_name = '_scope_view_{}' .format (s )
232+ scope_func = getattr (self , scope_name , None )
233+ if scope_func is None :
234+ raise UnexpectedScopeException (
235+ 'Scope {} is not implemented for model {}' .format (scope_name , self .model ))
236+
237+ result = scope_func (request )
238+
239+ # ignore scope functions which do not scope columns
240+ # i.e. they do not return a tuple of length two
241+ if isinstance (result , tuple ):
242+ if len (result ) < 2 :
243+ continue
244+
245+ columns = result [1 ]
246+ fields = append_columns (fields , columns ['fields' ])
247+ annotations = append_columns (annotations , columns ['annotations' ])
248+ properties = append_columns (properties , columns ['properties' ])
249+
250+ return fields , annotations , properties
251+
193252
194253 def _require_model_perm (self , perm_type , request , pk = None ):
195254 """
@@ -420,6 +479,13 @@ def scope_view(self, request, queryset):
420479 raise UnexpectedScopeException (
421480 'Scope {} is not implemented for model {}' .format (scope_name , self .model ))
422481 query_or_q = scope_func (request )
482+
483+ # view scoping may describe scoping of columns. In this case
484+ # the return type is a tuple and we only have to consider the
485+ # first argument
486+ if isinstance (query_or_q , tuple ):
487+ query_or_q = query_or_q [0 ]
488+
423489 # Allow either a ORM filter query manager or a Q object.
424490 # Q objects generate more efficient queries (so we don't
425491 # get an "id IN (subquery)"), but query managers allow
0 commit comments