44 HTTP_201_CREATED ,
55 HTTP_200_OK ,
66 HTTP_202_ACCEPTED ,
7- HTTP_404_NOT_FOUND
7+ HTTP_404_NOT_FOUND ,
88)
99from django .db .models import (
1010 Count ,
2424from attribute .models import Level , AttributeGroup , Attribute
2525from user .models import CustomUser
2626from json import loads
27- from typing import Any
27+ from typing import Any , Optional
2828from datetime import datetime as dt
2929from io import BytesIO
3030from .export import IMPLEMENTED , JSON , CSV , XLS
@@ -295,7 +295,105 @@ class StatsServices:
295295 )
296296
297297 @classmethod
298- def from_attribute (cls , project_id : int ) -> tuple [list [dict [str , Any ]], int ]:
298+ def from_diff (
299+ cls ,
300+ project_id : int ,
301+ diff_from : Optional [str ]
302+ ) -> tuple [list [dict [str , Any ]], int ]:
303+ # TODO: refactor, unify, if this shows legit data
304+ if not diff_from : return [], HTTP_200_OK
305+
306+ def helper (date : Optional [str ]) -> list [tuple [Any , ...]]:
307+ nonlocal project_id
308+ query = """
309+ select F.status, F.file_type, count(F.file_type) as count, L.order, L.name, A.id, A.name, A.parent_id, A.payload
310+ from file as F
311+ left join attribute_group AG on F.id = AG.file_id
312+ left join attribute_group_attribute as AGA on AGA.attributegroup_id = AG.uid
313+ left join attribute A on AGA.attribute_id = A.id
314+ left join level as L on L.id = A.level_id
315+ where F.project_id = {} {}
316+ group by A.id, F.file_type, F.status, L.id;
317+ """ .format (project_id , f"and update_date < '{ date } '" * bool (date ))
318+
319+ with connection .cursor () as cur :
320+ cur .execute (query , [])
321+ return cur .fetchall ()
322+
323+ stats_upto = helper (diff_from )
324+ stats_current = helper (None )
325+
326+ intermediate = {}
327+
328+ for row in stats_current :
329+ a_st , a_tp , cnt , order , l_name , a_id , a_name , a_par , a_payl = row
330+
331+ a_st = a_st or "v"
332+ a_name = a_name or "no attribute"
333+ a_tp = a_tp or "no type"
334+ order = order or 0
335+ l_name = l_name or "no level"
336+ a_id = a_id or "no id"
337+
338+ target = intermediate .get (a_id )
339+
340+ if not target : intermediate [a_id ] = {
341+ "id" : a_id ,
342+ "levelName" : l_name ,
343+ "name" : a_name ,
344+ "parent" : a_par ,
345+ "payload" : a_payl ,
346+ "order" : order ,
347+ a_st : {a_tp : cnt }
348+ }
349+ elif target .get (a_st ): target [a_st ][a_tp ] = target [a_st ].get (a_tp , 0 ) + cnt
350+ else : target [a_st ] = {a_tp : cnt }
351+
352+ for row in stats_upto :
353+ a_st , a_tp , cnt , order , l_name , a_id , a_name , a_par , a_payl = row
354+
355+ a_st = a_st or "v"
356+ a_name = a_name or "no attribute"
357+ a_tp = a_tp or "no type"
358+ order = order or 0
359+ l_name = l_name or "no level"
360+ a_id = a_id or "no id"
361+
362+ target = intermediate .get (a_id )
363+
364+ if not target : intermediate [a_id ] = {
365+ "id" : a_id ,
366+ "levelName" : l_name ,
367+ "name" : a_name ,
368+ "parent" : a_par ,
369+ "payload" : a_payl ,
370+ "order" : order ,
371+ a_st : {a_tp : - cnt }
372+ }
373+ elif target .get (a_st ): target [a_st ][a_tp ] = target [a_st ].get (a_tp , 0 ) - cnt
374+ else : target [a_st ] = {a_tp : - cnt }
375+
376+ items = list (intermediate .items ())
377+ for key , row in items :
378+ if not bool (
379+ row .get ("v" , {}).get ("image" , 0 )
380+ | row .get ("v" , {}).get ("video" , 0 )
381+ | row .get ("a" , {}).get ("image" , 0 )
382+ | row .get ("a" , {}).get ("video" , 0 )
383+ | row .get ("d" , {}).get ("image" , 0 )
384+ | row .get ("d" , {}).get ("video" , 0 )
385+ ):
386+ del intermediate [key ]
387+ continue
388+
389+ if parent := next ((p for p in intermediate .values () if p ["id" ] == row ["parent" ]), None ):
390+ parent ["children" ] = parent .get ("children" , []) + [row ]
391+ del intermediate [key ]
392+
393+ return sorted (intermediate .values (), key = lambda r : r ["order" ]), HTTP_200_OK
394+
395+ @classmethod
396+ def from_attribute (cls , project_id : int , * args ) -> tuple [list [dict [str , Any ]], int ]:
299397 stats : list [dict [str , Any ]] = list (
300398 Level .objects
301399 .filter (project_id = project_id )
@@ -345,7 +443,7 @@ def from_attribute(cls, project_id: int) -> tuple[list[dict[str, Any]], int]:
345443 return cls ._attribute_stat_adapt (stats ), HTTP_200_OK
346444
347445 @classmethod
348- def from_user (cls , project_id : int ):
446+ def from_user (cls , project_id : int , * args ):
349447 stats : list [dict [str , Any ]] = list (
350448 File .objects
351449 .filter (project_id = project_id )
@@ -427,7 +525,8 @@ def form_export_file(query: dict[str, Any]) -> BytesIO:
427525 query_set = {"type" , "project_id" , "choice" }
428526 choice_map = {
429527 "attribute" : StatsServices .from_attribute ,
430- "user" : StatsServices .from_user
528+ "user" : StatsServices .from_user ,
529+ "diff" : StatsServices .from_diff
431530 }
432531 export_map = {"json" : JSON , "csv" : CSV , "xlsx" : XLS }
433532
@@ -438,11 +537,12 @@ def form_export_file(query: dict[str, Any]) -> BytesIO:
438537 choice = query ["choice" ]
439538 project_id = query ["project_id" ]
440539 file_type = query ["type" ]
540+ diff_from = query .get ("diff_from" )
441541
442542 assert file_type in IMPLEMENTED , f"{ file_type } not implemented"
443- assert choice in set ( choice_map ), f"export for { choice } is not implemented"
543+ assert ( parser := choice_map . get ( choice ) ), f"export for { choice } is not implemented"
444544
445- stats , _ = choice_map [ choice ] (project_id )
545+ stats , _ = parser (project_id , diff_from )
446546 file = export_map [file_type ](stats , choice )
447547
448548 return file .into_response ()
0 commit comments