chellow 1759823276.0.0__py3-none-any.whl → 1761056531.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of chellow might be problematic. Click here for more details.

chellow/dloads.py CHANGED
@@ -91,7 +91,7 @@ class DloadFile:
91
91
  def open_file(base, user, mode="r", newline=None, is_zip=False):
92
92
  global download_id
93
93
 
94
- base = base.replace("/", "").replace(" ", "")
94
+ base = "".join(x if (x.isalnum() or x in "_.") else "_" for x in base)
95
95
  try:
96
96
  lock.acquire()
97
97
  if len(list(download_path.iterdir())) == 0:
@@ -108,7 +108,7 @@ def open_file(base, user, mode="r", newline=None, is_zip=False):
108
108
  un = user.proxy_username
109
109
  else:
110
110
  un = user.email_address
111
- uname = un.replace("@", "").replace(".", "").replace("\\", "")
111
+ uname = "".join(x if (x.isalnum() or x in "_") else "_" for x in un)
112
112
 
113
113
  names = tuple("_".join((serial, v, uname, base)) for v in ("RUNNING", "FINISHED"))
114
114
  running_name, finished_name = tuple(download_path / name for name in names)
chellow/e/lcc.py CHANGED
@@ -46,7 +46,7 @@ def run_import(sess, log, set_progress):
46
46
  s = requests.Session()
47
47
  s.verify = False
48
48
 
49
- for mod_name in ("chellow.e.cfd",):
49
+ for mod_name in ("chellow.e.cfd", "chellow.e.rab"):
50
50
  mod = import_module(mod_name)
51
51
  mod.lcc_import(sess, log, set_progress, s)
52
52
 
chellow/e/rab.py ADDED
@@ -0,0 +1,75 @@
1
+ from datetime import datetime as Datetime
2
+ from decimal import Decimal
3
+
4
+
5
+ from sqlalchemy import select
6
+
7
+ from chellow.e.lcc import api_records
8
+ from chellow.models import Contract, RateScript
9
+ from chellow.utils import ct_datetime, to_ct, to_utc
10
+
11
+
12
+ def hh(data_source):
13
+ try:
14
+ rab_cache = data_source.caches["rab"]
15
+ except KeyError:
16
+ rab_cache = data_source.caches["rab"] = {}
17
+
18
+ for h in data_source.hh_data:
19
+ try:
20
+ h["rab"] = rab_cache[h["start-date"]]
21
+ except KeyError:
22
+ h_start = h["start-date"]
23
+ rate_str = data_source.non_core_rate("rab_forecast_ilr_tra", h_start)[
24
+ "record"
25
+ ]["Interim_Levy_Rate_GBP_MWh"]
26
+ if rate_str == "":
27
+ base_rate_dec = Decimal("0")
28
+ else:
29
+ base_rate_dec = Decimal(rate_str) / Decimal(1000)
30
+
31
+ base_rate = float(base_rate_dec)
32
+
33
+ h["rab"] = rab_cache[h_start] = {
34
+ "interim": base_rate,
35
+ }
36
+
37
+
38
+ def lcc_import(sess, log, set_progress, s):
39
+ import_forecast_ilr_tra(sess, log, set_progress, s)
40
+
41
+
42
+ def _parse_date(date_str):
43
+ return to_utc(to_ct(Datetime.strptime(date_str[:10], "%Y-%m-%d")))
44
+
45
+
46
+ def import_forecast_ilr_tra(sess, log, set_progress, s):
47
+ log("Starting to check for new LCC RAB Forecast ILR TRA")
48
+
49
+ contract_name = "rab_forecast_ilr_tra"
50
+ contract = Contract.find_non_core_by_name(sess, contract_name)
51
+ if contract is None:
52
+ contract = Contract.insert_non_core(
53
+ sess, contract_name, "", {}, to_utc(ct_datetime(1996, 4, 1)), None, {}
54
+ )
55
+
56
+ for record in api_records(log, s, "1231fbb3-93ee-4a33-87a9-f15bb377346d"):
57
+ period_start_str = record["Month"]
58
+ if len(period_start_str) == 0:
59
+ continue
60
+ period_start = _parse_date(period_start_str)
61
+
62
+ rs = sess.execute(
63
+ select(RateScript).where(
64
+ RateScript.contract == contract,
65
+ RateScript.start_date == period_start,
66
+ )
67
+ ).scalar_one_or_none()
68
+ if rs is None:
69
+ rs = contract.insert_rate_script(sess, period_start, {})
70
+
71
+ rs_script = rs.make_script()
72
+ rs_script["record"] = record
73
+ rs.update(rs_script)
74
+ sess.commit()
75
+ log("Finished LCC RAB Forecast ILR TRA")
chellow/e/views.py CHANGED
@@ -381,7 +381,7 @@ def channel_snags_get():
381
381
  total_snags_q = total_snags_q.where(Era.dc_contract == contract)
382
382
  snags_q = snags_q.where(Era.dc_contract == contract)
383
383
 
384
- total_snags = g.sess.execute(total_snags_q)
384
+ total_snags = g.sess.scalars(total_snags_q).one()
385
385
 
386
386
  snag_groups = []
387
387
  prev_snag = None
@@ -1116,10 +1116,7 @@ def do_post(sess):
1116
1116
  exp_mpan_core if imp_mpan_core is None else imp_mpan_core
1117
1117
  ]
1118
1118
 
1119
- if "compression" in request.values:
1120
- compression = req_bool("compression")
1121
- else:
1122
- compression = True
1119
+ compression = req_bool("compression")
1123
1120
 
1124
1121
  user = g.user
1125
1122
 
@@ -0,0 +1,288 @@
1
+ import csv
2
+ import sys
3
+ import threading
4
+ import traceback
5
+
6
+ from dateutil.relativedelta import relativedelta
7
+
8
+ from flask import g, redirect, render_template
9
+
10
+ from sqlalchemy.orm import joinedload
11
+ from sqlalchemy.sql.expression import false, select, true
12
+
13
+ from werkzeug.exceptions import BadRequest
14
+
15
+ from chellow.dloads import open_file
16
+ from chellow.models import (
17
+ Channel,
18
+ Contract,
19
+ Era,
20
+ Party,
21
+ RSession,
22
+ Site,
23
+ SiteEra,
24
+ Snag,
25
+ Supply,
26
+ User,
27
+ )
28
+ from chellow.utils import (
29
+ csv_make_val,
30
+ hh_before,
31
+ req_bool,
32
+ req_int,
33
+ req_int_none,
34
+ req_str,
35
+ utc_datetime_now,
36
+ )
37
+
38
+
39
+ def _make_rows(
40
+ sess,
41
+ now,
42
+ contract,
43
+ days_hidden,
44
+ is_ignored,
45
+ show_settlement,
46
+ only_ongoing,
47
+ days_long_hidden,
48
+ limit=None,
49
+ ):
50
+ cutoff_date = now - relativedelta(days=days_hidden)
51
+ q = (
52
+ select(Snag, Channel, Era, Supply, SiteEra, Site, Contract)
53
+ .join(Channel, Snag.channel_id == Channel.id)
54
+ .join(Era, Channel.era_id == Era.id)
55
+ .join(Supply, Era.supply_id == Supply.id)
56
+ .join(SiteEra, Era.site_eras)
57
+ .join(Site, SiteEra.site_id == Site.id)
58
+ .join(Contract, Era.dc_contract_id == Contract.id)
59
+ .where(
60
+ SiteEra.is_physical == true(),
61
+ Snag.start_date < cutoff_date,
62
+ )
63
+ .order_by(
64
+ Site.code,
65
+ Supply.id,
66
+ Channel.imp_related,
67
+ Channel.channel_type,
68
+ Snag.description,
69
+ Snag.start_date,
70
+ Snag.id,
71
+ )
72
+ .options(joinedload(Era.dc_contract))
73
+ )
74
+ if contract is not None:
75
+ q = q.where(Era.dc_contract == contract)
76
+
77
+ if not is_ignored:
78
+ q = q.where(Snag.is_ignored == false())
79
+
80
+ if show_settlement == "yes":
81
+ q = q.join(Party, Supply.dno_id == Party.id).where(Party.dno_code != "99")
82
+ elif show_settlement == "no":
83
+ q = q.join(Party, Supply.dno_id == Party.id).where(Party.dno_code == "99")
84
+ elif show_settlement == "both":
85
+ pass
86
+ else:
87
+ raise BadRequest("show_settlement must be 'yes', 'no' or 'both'.")
88
+
89
+ snag_groups = []
90
+ prev_snag = None
91
+ for snag, channel, era, supply, site_era, site, contract in sess.execute(q):
92
+ snag_start = snag.start_date
93
+ snag_finish = snag.finish_date
94
+
95
+ if snag_finish is None:
96
+ duration = now - snag_start
97
+ age_of_snag = None
98
+ else:
99
+ duration = snag_finish - snag_start
100
+ if hh_before(cutoff_date, snag_finish):
101
+ age_of_snag = None
102
+ else:
103
+ delta = now - snag_finish
104
+ age_of_snag = delta.days
105
+
106
+ if only_ongoing and age_of_snag is not None:
107
+ continue
108
+
109
+ if days_long_hidden is not None and duration < days_long_hidden:
110
+ continue
111
+
112
+ if (
113
+ prev_snag is None
114
+ or channel.era != prev_snag.channel.era
115
+ or snag.start_date != prev_snag.start_date
116
+ or snag.finish_date != prev_snag.finish_date
117
+ or snag.description != prev_snag.description
118
+ ):
119
+ if limit is not None and len(snag_groups) > limit:
120
+ break
121
+
122
+ snag_group = {
123
+ "snags": [],
124
+ "site": site,
125
+ "era": era,
126
+ "supply": supply,
127
+ "description": snag.description,
128
+ "start_date": snag.start_date,
129
+ "finish_date": snag.finish_date,
130
+ "contract": contract,
131
+ "contract_name": contract.name,
132
+ "hidden_days": days_hidden,
133
+ "chellow_id": snag.id,
134
+ "imp_mpan_core": era.imp_mpan_core,
135
+ "exp_mpan_core": era.exp_mpan_core,
136
+ "site_code": site.code,
137
+ "site_name": site.name,
138
+ "snag_description": snag.description,
139
+ "channel_type": channel.channel_type,
140
+ "is_ignored": snag.is_ignored,
141
+ "days_since_finished": age_of_snag,
142
+ "duration": duration.days,
143
+ }
144
+ snag_groups.append(snag_group)
145
+ snag_group["snags"].append(snag)
146
+ prev_snag = snag
147
+
148
+ def make_key(item):
149
+ return item["duration"]
150
+
151
+ snag_groups.sort(key=make_key, reverse=True)
152
+ return snag_groups
153
+
154
+
155
+ def content(
156
+ contract_id,
157
+ days_hidden,
158
+ is_ignored,
159
+ user_id,
160
+ only_ongoing,
161
+ show_settlement,
162
+ days_long_hidden,
163
+ ):
164
+ f = writer = None
165
+ try:
166
+ with RSession() as sess:
167
+ user = User.get_by_id(sess, user_id)
168
+ if contract_id is None:
169
+ contract = None
170
+ namef = "all"
171
+ else:
172
+ contract = Contract.get_dc_by_id(sess, contract_id)
173
+ namef = contract.name
174
+
175
+ f = open_file(f"channel_snags_{namef}.csv", user, mode="w", newline="")
176
+ writer = csv.writer(f, lineterminator="\n")
177
+ titles = (
178
+ "contract",
179
+ "hidden_days",
180
+ "chellow_ids",
181
+ "imp_mpan_core",
182
+ "exp_mpan_core",
183
+ "site_code",
184
+ "site_name",
185
+ "snag_description",
186
+ "channel_types",
187
+ "start_date",
188
+ "finish_date",
189
+ "is_ignored",
190
+ "days_since_finished",
191
+ "duration",
192
+ )
193
+ writer.writerow(titles)
194
+
195
+ now = utc_datetime_now()
196
+ for snag_group in _make_rows(
197
+ sess,
198
+ now,
199
+ contract,
200
+ days_hidden,
201
+ is_ignored,
202
+ show_settlement,
203
+ only_ongoing,
204
+ days_long_hidden,
205
+ ):
206
+
207
+ vals = {
208
+ "contract": snag_group["contract"].name,
209
+ "hidden_days": days_hidden,
210
+ "chellow_ids": [snag.id for snag in snag_group["snags"]],
211
+ "imp_mpan_core": snag_group["imp_mpan_core"],
212
+ "exp_mpan_core": snag_group["exp_mpan_core"],
213
+ "site": snag_group["site"],
214
+ "site_code": snag_group["site"].code,
215
+ "site_name": snag_group["site"].name,
216
+ "snag_description": snag_group["description"],
217
+ "channel_types": [
218
+ f"{s.channel.imp_related}_{s.channel.channel_type}"
219
+ for s in snag_group["snags"]
220
+ ],
221
+ "start_date": snag_group["start_date"],
222
+ "finish_date": snag_group["finish_date"],
223
+ "is_ignored": snag_group["is_ignored"],
224
+ "days_since_finished": snag_group["days_since_finished"],
225
+ "duration": snag_group["duration"],
226
+ }
227
+
228
+ writer.writerow(csv_make_val(vals[t]) for t in titles)
229
+ except BaseException:
230
+ msg = traceback.format_exc()
231
+ sys.stderr.write(msg)
232
+ writer.writerow([msg])
233
+ finally:
234
+ if f is not None:
235
+ f.close()
236
+
237
+
238
+ LIMIT = 200
239
+
240
+
241
+ def do_get(sess):
242
+ contract_id = req_int_none("dc_contract_id")
243
+ days_hidden = req_int("days_hidden")
244
+ is_ignored = req_bool("is_ignored")
245
+ only_ongoing = req_bool("only_ongoing")
246
+ show_settlement = req_str("show_settlement")
247
+ as_csv = req_bool("as_csv")
248
+ days_long_hidden = req_int_none("days_long_hidden")
249
+
250
+ if as_csv:
251
+ args = (
252
+ contract_id,
253
+ days_hidden,
254
+ is_ignored,
255
+ g.user.id,
256
+ only_ongoing,
257
+ show_settlement,
258
+ )
259
+ threading.Thread(target=content, args=args).start()
260
+ return redirect("/downloads", 303)
261
+ else:
262
+ if contract_id is None:
263
+ contract = None
264
+ else:
265
+ contract = Contract.get_dc_by_id(sess, contract_id)
266
+ now = utc_datetime_now()
267
+ snag_groups = _make_rows(
268
+ sess,
269
+ now,
270
+ contract,
271
+ days_hidden,
272
+ is_ignored,
273
+ show_settlement,
274
+ only_ongoing,
275
+ days_long_hidden,
276
+ limit=LIMIT,
277
+ )
278
+ return render_template(
279
+ "reports/channel_snags.html",
280
+ contract=contract,
281
+ limit=LIMIT,
282
+ snag_groups=snag_groups,
283
+ days_hidden=days_hidden,
284
+ is_ignored=is_ignored,
285
+ only_ongoing=only_ongoing,
286
+ show_settlement=show_settlement,
287
+ days_long_hidden=days_long_hidden,
288
+ )
@@ -8,6 +8,15 @@
8
8
  Channel Snags
9
9
  {% endblock %}
10
10
 
11
+ {% block inside_head %}
12
+ <style>
13
+ #snags.htmx-swapping {
14
+ opacity: 0;
15
+ transition: opacity 1s ease-out;
16
+ }
17
+ </style>
18
+ {% endblock %}
19
+
11
20
  {% block nav %}
12
21
  <a href="/e/dc_contracts">DC Contracts</a> &raquo;
13
22
  {% if contract %}
@@ -17,113 +26,36 @@
17
26
  {% endblock %}
18
27
 
19
28
  {% block content %}
20
-
21
- <form action="/reports/233">
29
+ <form hx-get="/reports/channel_snags" hx-params="*" hx-include="this" hx-trigger="load, change"
30
+ hx-target="#snags" hx-swap="outerHTML swap:1s">
22
31
  <fieldset>
23
- <legend>Download CSV</legend>
32
+ <legend>Filters</legend>
24
33
  <input type="hidden" name="dc_contract_id" value="{{contract.id}}">
25
34
  <label>Hide snags &lt; days old</label>
26
- {{input_text('days_hidden', '0', 3, 3)}}
35
+ {{input_text('days_hidden', initial='5', size=3, maxlength=3)}}
27
36
  <label>Include ignored snags</label> {{input_checkbox('is_ignored', False)}}
28
- <input type="submit" value="Download">
29
- </fieldset>
30
- </form>
31
37
 
32
- <p>
33
- The 'Days Since Snag Finished' column is blank if the snag is ongoing (ie. if the
34
- finish date is 'ongoing' or if it finished less than 'Days Hidden' ago).
35
- </p>
38
+ <label>Only show ongoing snags</label>
39
+ {{input_checkbox('only_ongoing', initial=True)}}
36
40
 
37
- <form action="/e/channel_snags">
38
- <fieldset>
39
- <legend>Show Channel Snags</legend>
40
- <input type="hidden" name="dc_contract_id" value="
41
- {%- if contract -%}
42
- {{contract.id}}
43
- {%- endif -%}
44
- ">
45
- <label>Hide snags &lt; days old</label>
46
- {{input_text('days_hidden', '0', 3, 3)}}
47
- <label>Include ignored snags</label> {{input_checkbox('is_ignored', False)}}
48
- <input type="submit" value="Show">
49
- </fieldset>
50
- </form>
41
+ <fieldset>
42
+
43
+ <label>Show settlement only</label>
44
+ {{input_radio('show_settlement', 'yes', initial=False)}}
45
+
46
+ <label>Show non-settlement only</label>
47
+ {{input_radio('show_settlement', 'no', initial=False)}}
51
48
 
52
- <p>
53
- There are {{total_snags}} snag(s) older than {{request.values.days_hidden}} days
54
- {%- if not is_ignored %}
55
- that aren't ignored
56
- {%- endif -%}.
57
- </p>
49
+ <label>Show both</label>
50
+ {{input_radio('show_settlement', 'both', initial=True)}}
58
51
 
59
- <table>
60
- <caption>Snags (truncated after 200)</caption>
61
- <thead>
62
- <tr>
63
- <th>View</th>
64
- <th>Contract</th>
65
- <th>Import MPAN Core</th>
66
- <th>Export MPAN Core</th>
67
- <th>Sites</th>
68
- <th>Snag Description</th>
69
- <th>Channels</th>
70
- <th>Duration</th>
71
- </tr>
72
- </thead>
73
- <tbody>
74
- {% for snag_group in snag_groups %}
75
- <tr>
76
- <td>
77
- <ul>
78
- {% for snag in snag_group.snags %}
79
- <li>
80
- <a href="/e/channel_snags/{{snag.id}}">view</a>
81
- [<a href="/e/channel_snags/{{snag.id}}/edit">edit</a>]
82
- {% if snag.is_ignored %} ignored{% endif %}
83
- </li>
84
- {% endfor %}
85
- </ul>
86
- </td>
87
- <td>{{snag_group.era.dc_contract.name}}</td>
88
- <td>
89
- {% if snag_group.era.imp_mpan_core %}
90
- {{snag_group.era.imp_mpan_core}}
91
- {% endif %}
92
- </td>
93
- <td>
94
- {% if snag_group.era.exp_mpan_core %}
95
- {{snag_group.era.exp_mpan_core}}
96
- {% endif %}
97
- </td>
98
- <td>
99
- <ul>
100
- {% for site in snag_group.sites %}
101
- <li>{{site.code}} {{site.name}}</li>
102
- {% endfor %}
103
- </ul>
104
- </td>
105
- <td>{{snag_group.description}}</td>
106
- <td>
107
- <ul>
108
- {% for snag in snag_group.snags %}
109
- <li>
110
- {% if snag.channel.imp_related %}
111
- Import
112
- {% else %}
113
- Export
114
- {% endif %}
115
- {{snag.channel.channel_type}}
116
- </li>
117
- {% endfor %}
118
- </ul>
119
- </td>
120
- <td>
121
- {{snag_group.start_date|hh_format}} to
122
- {{snag_group.finish_date|hh_format}}
123
- </td>
124
- </tr>
125
- {% endfor %}
126
- </tbody>
127
- </table>
52
+ </fieldset>
53
+
54
+ <label>Hide snags &lt; days long</label>
55
+ {{input_text('days_long_hidden', initial='', size=3, maxlength=3)}}
56
+
57
+ </fieldset>
58
+ </form>
128
59
 
60
+ <div id="snags"></div>
129
61
  {% endblock %}
@@ -124,13 +124,23 @@
124
124
  </div>
125
125
  {%- endmacro -%}
126
126
 
127
- {%- macro input_checkbox(name, initial) %}
128
- <input type="checkbox" name="{{ name }}" value="true"
127
+ {%- macro input_checkbox(name, initial=False, value='true') %}
128
+ <input type="checkbox" name="{{ name }}" value="{{value}}"
129
129
  {%- if request.values[name] -%}
130
- {%- if request.values[name] == 'true' %} checked
130
+ {%- if request.values[name] == value %} checked
131
131
  {%- endif -%}
132
132
  {%- else -%}
133
- {%- if initial == True %} checked{% endif -%}
133
+ {%- if initial %} checked{% endif -%}
134
+ {%- endif -%}>
135
+ {%- endmacro -%}
136
+
137
+ {%- macro input_radio(name, value, initial=False) %}
138
+ <input type="radio" name="{{ name }}" value="{{value}}"
139
+ {%- if request.values[name] -%}
140
+ {%- if request.values[name] == value %} checked
141
+ {%- endif -%}
142
+ {%- else -%}
143
+ {%- if initial %} checked{% endif -%}
134
144
  {%- endif -%}>
135
145
  {%- endmacro -%}
136
146
 
@@ -0,0 +1,91 @@
1
+ {% extends "macros.html" %}
2
+
3
+ {% block html %}
4
+ <table class="sticky" id="snags">
5
+ <caption>
6
+ Snags (truncated after 200)
7
+ <a href="/reports/channel_snags?dc_contract_id={{contract.id}}&amp;days_hidden={{days_hidden}}&amp;is_ignored={{is_ignored}}&amp;only_ongoing={{only_ongoing}}&amp;show_settlement={{show_settlement}}&amp;as_csv=true">Download as CSV</a>
8
+ </caption>
9
+ <thead>
10
+ <tr>
11
+ <th>View</th>
12
+ <th>Contract</th>
13
+ <th>Import MPAN Core</th>
14
+ <th>Export MPAN Core</th>
15
+ <th>Site</th>
16
+ <th>Description</th>
17
+ <th>Channels</th>
18
+ <th>Start Date</th>
19
+ <th>Finish Date</th>
20
+ <th>
21
+ <span title="Blank if the snag is ongoing (ie. if the finish date is 'ongoing' or if it finished less than 'Days Hidden' ago).">Days Since Finished</span>
22
+ </th>
23
+ <th>Duration</th>
24
+ <th>Comm</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ {% for snag_group in snag_groups %}
29
+ <tr>
30
+ <td>
31
+ <ul>
32
+ {% for snag in snag_group.snags %}
33
+ <li>
34
+ <a href="/e/channel_snags/{{snag.id}}">view</a>
35
+ [<a href="/e/channel_snags/{{snag.id}}/edit">edit</a>]
36
+ {% if snag.is_ignored %} ignored{% endif %}
37
+ </li>
38
+ {% endfor %}
39
+ </ul>
40
+ </td>
41
+ <td>
42
+ <a href="/e/dc_contracts/{{snag_group.contract.id}}">{{snag_group.contract.name}}</a>
43
+ </td>
44
+ <td>
45
+ {% if snag_group.era.imp_mpan_core %}
46
+ <a href="/e/supplies/{{snag_group.supply.id}}">{{snag_group.era.imp_mpan_core}}</a>
47
+ {% endif %}
48
+ </td>
49
+ <td>
50
+ {% if snag_group.era.exp_mpan_core %}
51
+ <a href="/e/supplies/{{snag_group.supply.id}}">{{snag_group.era.exp_mpan_core}}</a>
52
+ {% endif %}
53
+ </td>
54
+ <td>
55
+ <a href="/sites/{{snag_group.site.id}}">{{snag_group.site.code}} {{snag_group.site.name}}</a>
56
+ </td>
57
+ <td>{{snag_group.description}}</td>
58
+ <td>
59
+ <ul>
60
+ {% for snag in snag_group.snags %}
61
+ <li>
62
+ {% if snag.channel.imp_related %}
63
+ Import
64
+ {% else %}
65
+ Export
66
+ {% endif %}
67
+ {{snag.channel.channel_type}}
68
+ </li>
69
+ {% endfor %}
70
+ </ul>
71
+ </td>
72
+ <td>
73
+ {{snag_group.start_date|hh_format}}
74
+ </td>
75
+ <td>
76
+ {{snag_group.finish_date|hh_format}}
77
+ </td>
78
+ <td>
79
+ {% if snag_group.days_since_finished %}
80
+ {{snag_group.days_since_finished}}
81
+ {% endif %}
82
+ </td>
83
+ <td>{{snag_group.duration}}</td>
84
+ <td>
85
+ <a href="/e/comms/{{snag_group.era.comm.id}}" title="{{snag_group.era.comm.description}}">{{snag_group.era.comm.code}}</a>
86
+ </td>
87
+ </tr>
88
+ {% endfor %}
89
+ </tbody>
90
+ </table>
91
+ {% endblock %}
chellow/utils.py CHANGED
@@ -29,11 +29,15 @@ def req_str(name):
29
29
  raise BadRequest(f"The field {name} is required.")
30
30
 
31
31
 
32
- def req_bool(name):
32
+ def req_strs(name):
33
33
  try:
34
- return request.values[name] == "true"
34
+ return request.values.getlist(name)
35
35
  except KeyError:
36
- return False
36
+ raise BadRequest(f"The field {name} is required.")
37
+
38
+
39
+ def req_bool(name):
40
+ return name in request.values
37
41
 
38
42
 
39
43
  def req_int(name):
@@ -43,6 +47,13 @@ def req_int(name):
43
47
  raise BadRequest(f"Problem parsing the field {name} as an integer: {e}")
44
48
 
45
49
 
50
+ def req_ints(name):
51
+ try:
52
+ return [int(v) for v in req_strs(name)]
53
+ except ValueError as e:
54
+ raise BadRequest(f"Problem parsing the field {name} as an integer: {e}")
55
+
56
+
46
57
  def req_int_none(name):
47
58
  val = req_str(name)
48
59
  if val == "":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chellow
3
- Version: 1759823276.0.0
3
+ Version: 1761056531.0.0
4
4
  Summary: Web Application for checking UK energy bills.
5
5
  Project-URL: Homepage, https://github.com/WessexWater/chellow
6
6
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
@@ -2,7 +2,7 @@ chellow/__init__.py,sha256=9aoSbGmHCIHwzI_c_TLSg5CeKwqAq0I6kKaMvTrTMqg,10936
2
2
  chellow/api.py,sha256=mk17TfweR76DPFC8lX2SArTjai6y6YshASxqO1w-_-s,11036
3
3
  chellow/bank_holidays.py,sha256=T_utYMwe_g1dz5X-aOTdIPryg49SvB7QsWM1yphlqG8,4423
4
4
  chellow/commands.py,sha256=ESBe9ZWj1c3vdZgqMZ9gFvYAB3hRag2R1PzOwuw9yFo,1302
5
- chellow/dloads.py,sha256=dixp-O0MF2_mlwrnKx3D9DH09Qu05BjTo0rZfigTjR4,5534
5
+ chellow/dloads.py,sha256=sNAPMe4LeHqfisEubGXvraDsUS0F-ujI3WG0Md8DywM,5565
6
6
  chellow/edi_lib.py,sha256=Lq70TUJuogoP5KGrphzUEUfyfgftEclg_iA3mpNAaDI,51324
7
7
  chellow/fake_batch_updater.py,sha256=khAmvSUn9qN04w8C92kRg1UeyQvfLztE7QXv9tUz6nE,11611
8
8
  chellow/general_import.py,sha256=ghybbden66VT4q5J0vYwiNg-6G2vg71EgCN_x3fvhW0,69200
@@ -12,7 +12,7 @@ chellow/proxy.py,sha256=cVXIktPlX3tQ1BYcwxq0nJXKE6r3DtFTtfFHPq55HaM,1351
12
12
  chellow/rate_server.py,sha256=RwJo-AzBIdzxx7PAtboZEUH1nUjAeJckw0bk06-9oyM,5672
13
13
  chellow/rrun.py,sha256=sWm_tuJ_6xH3T28TY1w63k1Q44N_S_p_pYF5YCeztqU,2226
14
14
  chellow/testing.py,sha256=Dj2c1NX8lVlygueOrh2eyYawLW6qKEHxNhXVVUaNRO0,3637
15
- chellow/utils.py,sha256=i3GQK9MIcweosZk2gi-nX_IFq2DxURAJDyNoLBg6YwM,19421
15
+ chellow/utils.py,sha256=x6yReQkhFaFGnsuhs6PqrEkE1OfZSPrrC7IqstRXKKU,19701
16
16
  chellow/views.py,sha256=9IUUbYjqEmQQzbYQB4w-ygCo25gecIxYVbxTXRF-YfY,86078
17
17
  chellow/e/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  chellow/e/aahedc.py,sha256=d2usudp7KYWpU6Pk3fal5EQ47EbvkvKeaFGylnb3NWw,606
@@ -35,8 +35,9 @@ chellow/e/hh_parser_schneider_xlsx.py,sha256=Vtq0TNz-oojoKJm4PeH4ZwBp2I-mjArB9-F
35
35
  chellow/e/hh_parser_simple_csv.py,sha256=lJx9tw9BWFSoBmns1Cws_vY-OIn90LPt2yvIN_CFcTE,2177
36
36
  chellow/e/hh_parser_vital_xlsx.py,sha256=g9-CElfH1PPfwpuUcVvD6WQpBlNxCo8j9pq_0Yza0ZM,4125
37
37
  chellow/e/lafs.py,sha256=SUUFtvn_IQQTrZue1zefCYgzuscA0FVX2ySZ9u8BDPw,7776
38
- chellow/e/lcc.py,sha256=OkpynN8_iAdHRlu-yyU6BhRUqYYOZsUnl0HbHULYo_4,4670
38
+ chellow/e/lcc.py,sha256=VqwKBE6N6wXv_xu0o-1CRtJripoFGCZ8Q8e1hvce_34,4686
39
39
  chellow/e/mdd_importer.py,sha256=NugJr2JhuzkPTsEMl_5UdQuw5K2p8lVJ-hyz4MK6Hfg,35762
40
+ chellow/e/rab.py,sha256=8buWWqo7CPzAqenI8zEnKDMVxzXZzQZUFAWY9sdGH3s,2320
40
41
  chellow/e/rcrc.py,sha256=92CA1uIotIHd1epQ_jEPdJKzXqDFV-AoJOJeRO6MEyA,4274
41
42
  chellow/e/ro.py,sha256=cpeJQMY_6SpVxL1nySSj9we58A6E2KwJEAb6fK7cZKY,626
42
43
  chellow/e/scenario.py,sha256=FLgh03r_SgXx0hMWFbAvwsz2ScDL8LUwYWSWVv2rQlg,24973
@@ -44,7 +45,7 @@ chellow/e/system_price.py,sha256=6w5J7bzwFAZubE2zdOFRiS8IIrVP8hkoIOaG2yCt-Ic,623
44
45
  chellow/e/tlms.py,sha256=pyL32hPiYd09FbpXVMnBjHsWFEQHs-Az945V7EShGiY,9116
45
46
  chellow/e/tnuos.py,sha256=NBmc-f3oezrl4gviAKobljHfICTpBKxxxEGBGJi_lRk,4927
46
47
  chellow/e/triad.py,sha256=uQIngSrz8irBXQ0Rp_s8nAUzu-y2Ms7aj4B38_Ff8y8,13720
47
- chellow/e/views.py,sha256=UH9Wc6UOiCcs9OtXWU8MK2eBi9d_J2xJN6qmHgZVat0,234245
48
+ chellow/e/views.py,sha256=7WKLrqYtk018oXjmx9YBv72B5hpgOV0eTJYqsIoBYsE,234251
48
49
  chellow/e/bill_parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  chellow/e/bill_parsers/activity_mop_stark_xlsx.py,sha256=hcbjxqLOe7qkDjS7enCpmfyGwm3d-pq3u5VQIaUmVTw,3796
50
51
  chellow/e/bill_parsers/annual_mop_stark_xlsx.py,sha256=yoEGQS0qcrv3TWFfxELIIi8f1CyKcIzh0xVaPoz2w2s,4191
@@ -85,9 +86,8 @@ chellow/reports/report_183.py,sha256=fGONpKEIXTqIT_3dE5jZKBIesWPgLq-chHO6X22dIv0
85
86
  chellow/reports/report_187.py,sha256=RCvxDnkfRu7ocKUPEX0qtVphGOBEl9RULy790uteXcQ,9915
86
87
  chellow/reports/report_219.py,sha256=3Zs16ka6dWWY_lCDoA-ZFrlPb4vHk1LQoa2yrlTMntA,7117
87
88
  chellow/reports/report_231.py,sha256=WARLztV2HzBFSi5_xvY_DTXVyuBBbBYiU0RazfjKw-Y,5270
88
- chellow/reports/report_233.py,sha256=qJdPFuBgQAWD24BsljgGJ44L_RGdHOA1RqsxGjTWhFc,4975
89
89
  chellow/reports/report_241.py,sha256=lfivElRkV_CZAhck0rxAuhqzgponj3aWwmwGMNQvUxg,5343
90
- chellow/reports/report_247.py,sha256=KuNCWfYt1WByFv3EfiJF1-5blhJVWLbAdhlI5KD9HxQ,43346
90
+ chellow/reports/report_247.py,sha256=pbWfM-fUeuakZxH1Ovlx7-XfUg6SL9t51swK3yMc1lc,43253
91
91
  chellow/reports/report_29.py,sha256=ZXnq6Kg5on-IPquUsH4bn46mBVESY_WhwqDWZPXOJwo,2695
92
92
  chellow/reports/report_291.py,sha256=BnWtxe0eWN2QKKWpwjs5-RI5LbReBKL119QbkrkNhV8,7478
93
93
  chellow/reports/report_33.py,sha256=lt1EN_LNx6u-AgdaS3YRkPMZA33JgMcELolHF4oJUMw,16689
@@ -100,6 +100,7 @@ chellow/reports/report_87.py,sha256=udzbCuXcckWD-OHmfJCT6bwg_paYhm4vfDWlL8WM-jA,
100
100
  chellow/reports/report_asset_comparison.py,sha256=Cl2NgvbclqhOVvKuUu3sajTVO2JupMfzK3bV0_K8eNs,6077
101
101
  chellow/reports/report_batches.py,sha256=MoCv2dE-JgaJzaMjMA-kZrPkYR13uDoXer5UuF12OYc,4757
102
102
  chellow/reports/report_bills.py,sha256=LP7XDxzO0Fp10c8xDE67e4tHTEc7nN74ESQBy762Nx4,3401
103
+ chellow/reports/report_channel_snags.py,sha256=RV5L3zRwFmuZws7w9H9VzLCFlQ6U3-ZU6jn4j5VydNc,8626
103
104
  chellow/reports/report_csv_llfcs.py,sha256=mMB06b6Jems5kcQU4H4Le_fyKgVr8THP8BCx3pkvg5E,1903
104
105
  chellow/reports/report_csv_site_hh_data.py,sha256=ik0OkGVo1bfTXLdcT3gPUHnxSkSdorzZheP3qgnOR5c,4331
105
106
  chellow/reports/report_csv_site_snags.py,sha256=AuAy6vjL0g7vwPPAZhBqxOyITL9_jnyFj012MnUcxxk,2627
@@ -133,7 +134,7 @@ chellow/templates/home.html,sha256=7xiD6QLju3ugALzMlzTJosRyMUmQGWPK6jDF7oZbnAQ,5
133
134
  chellow/templates/input_date.html,sha256=rpgB5n0LfN8Y5djN_ZiuSxqdskxzCoKrEqI7hyJkVQo,1248
134
135
  chellow/templates/local_report.html,sha256=pV7_0QwyQ-D3OS9LXrly5pq3qprZnwTCoq6vCnMTkS4,1332
135
136
  chellow/templates/local_reports.html,sha256=4wbfVkY4wUfSSfWjxqIsvCpIsa9k7H_dGAjznrG5jNM,701
136
- chellow/templates/macros.html,sha256=zd5P3irbRMXiK6LnWdbv5M0SYX2roZKhEzNTt2_uPyM,5031
137
+ chellow/templates/macros.html,sha256=FlRTl9W2avRXErnS3vzeOxciGXJYoKptiR08iVmSpfw,5353
137
138
  chellow/templates/national_grid.html,sha256=8W92tsjlqPSB0J--nyFIi-wzFae9CyDr6fODXLZp8ic,991
138
139
  chellow/templates/non_core_auto_importer.html,sha256=s9SluRN1bHTHjd8GP6uFhk6LrZYG8dqBG3y94zUKe5Q,944
139
140
  chellow/templates/non_core_contract.html,sha256=BdGtxErTvw4DwXLb6B2vimuU6ode3fFA-90kBmHCwYc,1754
@@ -180,7 +181,7 @@ chellow/templates/e/channel_add.html,sha256=szwQJBHx2kAoSFomXDHD0N_7PSd4drqOoAWh
180
181
  chellow/templates/e/channel_edit.html,sha256=OUkdiS2NBQ_w4gmxRxXAcRToM5BT8DWWJrtQMFs-GUU,2198
181
182
  chellow/templates/e/channel_snag.html,sha256=wBJ5KBfeJdAeRmaB0qu0AD9Z4nM5fn6tJ_8bNqTWtoo,1477
182
183
  chellow/templates/e/channel_snag_edit.html,sha256=sUFI4Ml3F1B35x8_t_Pz3hWuQN9Xtxr3kt4u8hdxNwY,1911
183
- chellow/templates/e/channel_snags.html,sha256=PuYnDycsQcwmmW9TfCCa_WgJRHOFn9HAnNm51rH7l9k,3157
184
+ chellow/templates/e/channel_snags.html,sha256=TgRhS6DcOMyoAowwrCjPtq9iuX6lpvG3uchcd3g80kk,1593
184
185
  chellow/templates/e/comm.html,sha256=DSlAaDg1r4KYq9VUgDtt2lgW6apZjZvwhMujIJINmps,383
185
186
  chellow/templates/e/comms.html,sha256=bUXZePnMfpKk1E71qLGOSkx8r3GxdPPD_-MosIXXq4s,434
186
187
  chellow/templates/e/cop.html,sha256=ULv7ALFJHMUgPX96hQNm2irc3edtKYHS6fYAxvmzj8M,376
@@ -399,6 +400,7 @@ chellow/templates/g/supply_note_edit.html,sha256=b8mB6_ucBwoljp03iy6AgVaZUhGw3-1
399
400
  chellow/templates/g/supply_notes.html,sha256=6epNmZ3NKdXZz27fvmRUGeffg_oc1kmwuBeyRzQe3Rg,854
400
401
  chellow/templates/g/unit.html,sha256=KouNVU0-i84afANkLQ_heJ0uDfJ9H5A05PuLqb8iCN8,438
401
402
  chellow/templates/g/units.html,sha256=p5Nd-lAIboKPEOO6N451hx1bcKxMg4BDODnZ-43MmJc,441
402
- chellow-1759823276.0.0.dist-info/METADATA,sha256=FByd2AG_PHGsgs27uxDhVTHa9U52X14rX8HGh4X7aVo,12500
403
- chellow-1759823276.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
404
- chellow-1759823276.0.0.dist-info/RECORD,,
403
+ chellow/templates/reports/channel_snags.html,sha256=_aAgFgMlTkK2HuKFU8YisAIcUYfg6Hqhgyf5MZpdK8c,2629
404
+ chellow-1761056531.0.0.dist-info/METADATA,sha256=x465BtJg_0nCB84ep2IVjHyEpTTyZZixgcyJ_zKU7fk,12500
405
+ chellow-1761056531.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
406
+ chellow-1761056531.0.0.dist-info/RECORD,,
@@ -1,150 +0,0 @@
1
- import csv
2
- import sys
3
- import threading
4
- import traceback
5
-
6
- from dateutil.relativedelta import relativedelta
7
-
8
- from flask import g, redirect
9
-
10
- from sqlalchemy.orm import joinedload
11
- from sqlalchemy.sql.expression import false, select, true
12
-
13
- from chellow.dloads import open_file
14
- from chellow.models import (
15
- Channel,
16
- Contract,
17
- Era,
18
- Session,
19
- Site,
20
- SiteEra,
21
- Snag,
22
- Supply,
23
- User,
24
- )
25
- from chellow.utils import (
26
- csv_make_val,
27
- hh_before,
28
- req_bool,
29
- req_int,
30
- req_int_none,
31
- utc_datetime_now,
32
- )
33
-
34
-
35
- def content(contract_id, days_hidden, is_ignored, user_id):
36
- f = writer = None
37
- try:
38
- with Session() as sess:
39
- user = User.get_by_id(sess, user_id)
40
- if contract_id is None:
41
- contract = None
42
- namef = "all"
43
- else:
44
- contract = Contract.get_dc_by_id(sess, contract_id)
45
- namef = "".join(x if x.isalnum() else "_" for x in contract.name)
46
-
47
- f = open_file(f"channel_snags_{namef}.csv", user, mode="w", newline="")
48
- writer = csv.writer(f, lineterminator="\n")
49
- titles = (
50
- "contract",
51
- "Hidden Days",
52
- "Chellow Id",
53
- "Imp MPAN Core",
54
- "Exp MPAN Core",
55
- "Site Code",
56
- "Site Name",
57
- "Snag Description",
58
- "Import Related?",
59
- "Channel Type",
60
- "Start Date",
61
- "Finish Date",
62
- "Is Ignored?",
63
- "Days Since Snag Finished",
64
- "Duration Of Snag (Days)",
65
- )
66
- writer.writerow(titles)
67
-
68
- now = utc_datetime_now()
69
- cutoff_date = now - relativedelta(days=days_hidden)
70
- q = (
71
- select(Snag, Channel, Era, Supply, SiteEra, Site)
72
- .join(Channel, Snag.channel_id == Channel.id)
73
- .join(Era, Channel.era_id == Era.id)
74
- .join(Supply, Era.supply_id == Supply.id)
75
- .join(SiteEra, Era.site_eras)
76
- .join(Site, SiteEra.site_id == Site.id)
77
- .where(
78
- SiteEra.is_physical == true(),
79
- Snag.start_date < cutoff_date,
80
- )
81
- .order_by(
82
- Site.code,
83
- Supply.id,
84
- Channel.imp_related,
85
- Channel.channel_type,
86
- Snag.description,
87
- Snag.start_date,
88
- Snag.id,
89
- )
90
- .options(joinedload(Era.dc_contract))
91
- )
92
- if contract is not None:
93
- q = q.where(Era.dc_contract == contract)
94
-
95
- if not is_ignored:
96
- q = q.where(Snag.is_ignored == false())
97
-
98
- for snag, channel, era, supply, site_era, site in sess.execute(q):
99
- snag_start = snag.start_date
100
- snag_finish = snag.finish_date
101
- imp_mc = "" if era.imp_mpan_core is None else era.imp_mpan_core
102
- exp_mc = "" if era.exp_mpan_core is None else era.exp_mpan_core
103
-
104
- if snag_finish is None:
105
- duration = now - snag_start
106
- age_of_snag = None
107
- else:
108
- duration = snag_finish - snag_start
109
- if hh_before(cutoff_date, snag_finish):
110
- age_of_snag = None
111
- else:
112
- delta = now - snag_finish
113
- age_of_snag = delta.days
114
-
115
- vals = {
116
- "contract": era.dc_contract.name,
117
- "Hidden Days": days_hidden,
118
- "Chellow Id": snag.id,
119
- "Imp MPAN Core": imp_mc,
120
- "Exp MPAN Core": exp_mc,
121
- "Site Code": site.code,
122
- "Site Name": site.name,
123
- "Snag Description": snag.description,
124
- "Import Related?": channel.imp_related,
125
- "Channel Type": channel.channel_type,
126
- "Start Date": snag_start,
127
- "Finish Date": snag_finish,
128
- "Is Ignored?": snag.is_ignored,
129
- "Days Since Snag Finished": age_of_snag,
130
- "Duration Of Snag (Days)": duration.days,
131
- }
132
-
133
- writer.writerow(csv_make_val(vals[t]) for t in titles)
134
- except BaseException:
135
- msg = traceback.format_exc()
136
- sys.stderr.write(msg)
137
- writer.writerow([msg])
138
- finally:
139
- if f is not None:
140
- f.close()
141
-
142
-
143
- def do_get(sess):
144
- contract_id = req_int_none("dc_contract_id")
145
- days_hidden = req_int("days_hidden")
146
- is_ignored = req_bool("is_ignored")
147
-
148
- args = contract_id, days_hidden, is_ignored, g.user.id
149
- threading.Thread(target=content, args=args).start()
150
- return redirect("/downloads", 303)