Skip to content

Commit 14b548e

Browse files
committed
CHANGES:
- Statistics: New periods 'quarter' and 'halfyear'
1 parent a67e516 commit 14b548e

File tree

2 files changed

+144
-42
lines changed

2 files changed

+144
-42
lines changed

plugins/competitive/reports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async def render(self, match: dict):
9090
class History(report.GraphElement):
9191

9292
async def render(self, ucid: str, name: str, flt: StatisticsFilter):
93-
self.env.embed.title = flt.format(self.bot) + ' ' + self.env.embed.title
93+
self.env.embed.title = flt.format(self.bot) + self.env.embed.title
9494
query = f"""
9595
SELECT time, skill_mu, skill_sigma FROM (
9696
SELECT time, skill_mu, skill_sigma

plugins/userstats/filter.py

Lines changed: 143 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,40 @@ class PeriodFilter(StatisticsFilter):
5858

5959
@staticmethod
6060
def list(bot: DCSServerBot) -> list[str]:
61-
return ['all', 'day', 'week', 'month', 'year', 'today', 'yesterday']
61+
"""
62+
All period names that can be passed directly to the filter.
63+
"""
64+
return [
65+
'all',
66+
'today',
67+
'yesterday',
68+
'day',
69+
'week',
70+
'month',
71+
'quarter',
72+
'halfyear',
73+
'year',
74+
]
6275

6376
@staticmethod
6477
def supports(bot: DCSServerBot, period: str) -> bool:
65-
return (period and period.startswith('period:')) or period in PeriodFilter.list(bot) or '-' in period
78+
"""
79+
A period is supported if:
80+
• it starts with 'period:' (custom range syntax)
81+
• it is one of the names in .list()
82+
• it contains a hyphen and the hyphen syntax is valid
83+
"""
84+
return (
85+
(period and period.startswith('period:')) or
86+
period in PeriodFilter.list(bot) or
87+
'-' in period
88+
)
6689

6790
@staticmethod
68-
def parse_date(date_str):
91+
def parse_date(date_str: str) -> datetime:
92+
"""
93+
Accepts YYYYMMDD, YYYYMMDD HH, etc. The format list is unchanged.
94+
"""
6995
formats = [
7096
"%Y%m%d %H:%M:%S",
7197
"%Y%m%d %H:%M",
@@ -74,63 +100,93 @@ def parse_date(date_str):
74100
"%Y%m",
75101
"%Y"
76102
]
77-
78103
for fmt in formats:
79104
try:
80105
return datetime.strptime(date_str, fmt)
81106
except ValueError:
82107
continue
83-
84-
# If none of the formats match, raise an error
85108
raise ValueError(f"Date format {date_str} is not supported")
86109

110+
@staticmethod
111+
def _interval_from_period(period: str) -> str:
112+
"""
113+
Convert a public period name into a Postgres interval literal.
114+
Unsupported names fall back to the literal itself (e.g. '1 week').
115+
"""
116+
mapping = {
117+
'quarter': '3 months',
118+
'halfyear': '6 months'
119+
}
120+
return mapping.get(period, f"1 {period}")
121+
87122
def filter(self, bot: DCSServerBot) -> str:
88-
if self.period and self.period.startswith('period:'):
89-
period = self.period[7:].strip()
90-
else:
91-
period = self.period
92-
if period in [None, 'all']:
123+
# Normalize the period string
124+
period = self.period[7:].strip() if self.period and self.period.startswith('period:') else self.period
125+
126+
# ------------------------------------------------------------------
127+
# 1 Handle the “all” case – no filtering
128+
# ------------------------------------------------------------------
129+
if period in (None, 'all'):
93130
return '1 = 1'
94-
elif period == 'yesterday':
131+
132+
# ------------------------------------------------------------------
133+
# 2 Special dates (today / yesterday)
134+
# ------------------------------------------------------------------
135+
if period == 'yesterday':
95136
return "DATE_TRUNC('day', s.hop_on) = current_date - 1"
96137
elif period == 'today':
97138
return "DATE_TRUNC('day', s.hop_on) = current_date"
98-
elif period in PeriodFilter.list(bot):
99-
return f"s.hop_on > ((now() AT TIME ZONE 'utc') - interval '1 {period}')"
100-
elif '-' in period:
101-
start, end = period.split('-')
102-
start = start.strip()
103-
end = end.strip()
104-
# avoid SQL injection
105-
pattern = re.compile(r'^\d+\s+(year|month|week|day|hour|minute)s?$')
139+
140+
# ------------------------------------------------------------------
141+
# 3 One‑step intervals: day, week, month, quarter, year
142+
# ------------------------------------------------------------------
143+
if period in PeriodFilter.list(bot):
144+
# Translate friendly name → Postgres intervals
145+
interval_lit = PeriodFilter._interval_from_period(period)
146+
return f"s.hop_on > ((now() AT TIME ZONE 'utc') - interval '{interval_lit}')"
147+
148+
# ------------------------------------------------------------------
149+
# 4 Custom “start‑end” syntax
150+
# ------------------------------------------------------------------
151+
if '-' in period:
152+
start, end = [p.strip() for p in period.split('-', 1)]
153+
154+
# Pattern for “X unit” (e.g. “2 week”, “5 days”)
155+
pattern = re.compile(r'^\d+\s+(year|month|week|day|hour|minute|quarter|halfyear)s?$')
106156
if pattern.match(end):
107157
return f"s.hop_on > ((now() AT TIME ZONE 'utc') - interval '{end}')"
108-
else:
109-
start = self.parse_date(start) if start else datetime(year=1970, month=1, day=1)
110-
end = self.parse_date(end) if end else datetime.now(tz=timezone.utc)
111-
return (f"s.hop_on >= '{start.strftime('%Y-%m-%d %H:%M:%S')}'::TIMESTAMP AND "
112-
f"COALESCE(s.hop_off, (now() AT TIME ZONE 'utc')) <= '{end.strftime('%Y-%m-%d %H:%M:%S')}'")
113-
else:
114-
return "1 = 1"
115158

159+
# Otherwise treat both sides as dates
160+
start_dt = self.parse_date(start) if start else datetime(year=1970, month=1, day=1)
161+
end_dt = self.parse_date(end) if end else datetime.now(tz=timezone.utc)
162+
163+
return (
164+
f"s.hop_on >= '{start_dt.strftime('%Y-%m-%d %H:%M:%S')}'::TIMESTAMP "
165+
f"AND COALESCE(s.hop_off, (now() AT TIME ZONE 'utc')) <= "
166+
f"'{end_dt.strftime('%Y-%m-%d %H:%M:%S')}'"
167+
)
168+
169+
# ------------------------------------------------------------------
170+
# 5 Fallback – no filtering
171+
# ------------------------------------------------------------------
172+
return "1 = 1"
116173

117174
def format(self, bot: DCSServerBot) -> str:
118-
if self.period and self.period.startswith('period:'):
119-
period = self.period[7:]
120-
else:
121-
period = self.period
122-
if period in [None, 'all']:
175+
period = self.period[7:] if self.period and self.period.startswith('period:') else self.period
176+
177+
if period in (None, 'all'):
123178
return 'Overall '
124-
elif period == 'day':
125-
return 'Daily '
126-
elif period in ['today', 'yesterday']:
179+
elif period in ('today', 'yesterday'):
127180
return period.capitalize() + 's '
128-
elif period in ['day', 'week', 'month', 'year']:
181+
elif period in ('day', 'week', 'month', 'year', 'quarter'):
182+
# The last part of every name is turned into an adjective
129183
return period.capitalize() + 'ly '
184+
elif period == 'halfyear':
185+
return period.capitalize() + ' '
130186
elif '-' in period:
131187
return period + '\n'
132-
else:
133-
return period
188+
189+
return period
134190

135191

136192
class CampaignFilter(StatisticsFilter):
@@ -282,11 +338,57 @@ def format(self, bot: DCSServerBot) -> str:
282338
class MissionStatisticsFilter(PeriodFilter):
283339

284340
def filter(self, bot: DCSServerBot) -> str:
285-
if self.period in self.list(bot) and self.period.lower() != 'all':
286-
return f"time > ((now() AT TIME ZONE 'utc') - interval '1 {self.period}')"
287-
else:
341+
# Normalize the period string
342+
period = self.period[7:].strip() if self.period and self.period.startswith('period:') else self.period
343+
344+
# ------------------------------------------------------------------
345+
# 1 Handle the “all” case – no filtering
346+
# ------------------------------------------------------------------
347+
if period in (None, 'all'):
288348
return '1 = 1'
289349

350+
# ------------------------------------------------------------------
351+
# 2 Special dates (today / yesterday)
352+
# ------------------------------------------------------------------
353+
if period == 'yesterday':
354+
return "DATE_TRUNC('day', time) = current_date - 1"
355+
elif period == 'today':
356+
return "DATE_TRUNC('day', time) = current_date"
357+
358+
# ------------------------------------------------------------------
359+
# 3 One‑step intervals: day, week, month, quarter, year
360+
# ------------------------------------------------------------------
361+
if period in PeriodFilter.list(bot):
362+
# Translate friendly name → Postgres intervals
363+
interval_lit = PeriodFilter._interval_from_period(period)
364+
return f"time > ((now() AT TIME ZONE 'utc') - interval '{interval_lit}')"
365+
366+
# ------------------------------------------------------------------
367+
# 4 Custom “start‑end” syntax
368+
# ------------------------------------------------------------------
369+
if '-' in period:
370+
start, end = [p.strip() for p in period.split('-', 1)]
371+
372+
# Pattern for “X unit” (e.g. “2 week”, “5 days”)
373+
pattern = re.compile(r'^\d+\s+(year|month|week|day|hour|minute|quarter)s?$')
374+
if pattern.match(end):
375+
return f"time > ((now() AT TIME ZONE 'utc') - interval '{end}')"
376+
377+
# Otherwise treat both sides as dates
378+
start_dt = self.parse_date(start) if start else datetime(year=1970, month=1, day=1)
379+
end_dt = self.parse_date(end) if end else datetime.now(tz=timezone.utc)
380+
381+
return (
382+
f"time >= '{start_dt.strftime('%Y-%m-%d %H:%M:%S')}'::TIMESTAMP "
383+
f"AND time <= '{end_dt.strftime('%Y-%m-%d %H:%M:%S')}'"
384+
)
385+
386+
# ------------------------------------------------------------------
387+
# 5 Fallback – no filtering
388+
# ------------------------------------------------------------------
389+
return "1 = 1"
390+
391+
290392
class StatsPagination(Pagination):
291393
def __init__(self, env: ReportEnv):
292394
super().__init__(env)

0 commit comments

Comments
 (0)