Only allow floats in LonLat (and not None, None).
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrmwcache.py
1 """Contains functions that maintain/update the cache tables."""
2 from sqlalchemy import schema
3 from sqlalchemy.sql import select
4 from sqlalchemy.sql.expression import func as sqlfunc
5 from osgeo import ogr
6 from wrpylib import mwdb, wrmwdb, wrmwmarkup, wrvalidators
7
8
9 class UpdateCacheError(RuntimeError):
10     pass
11
12
13 def update_wrsledruncache(connection):
14     """Updates the wrsledruncache table from the wiki. If convert errors occur, an UpdateCacheError exception
15     is raised. No other exception type should be raised under normal circumstances.
16     
17     >>> from sqlalchemy.engine import create_engine
18     >>> engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4')
19     >>> update_wrsledruncache(engine.connect())
20     """
21     metadata = schema.MetaData()
22     wrsledruncache = wrmwdb.wrsledruncache_table(metadata)
23     page = mwdb.page_table(metadata)
24     categorylinks = mwdb.categorylinks_table(metadata)
25     revision = mwdb.revision_table(metadata)
26     text = mwdb.text_table(metadata)
27
28     class Sledrun:
29         pass
30
31     transaction = connection.begin()
32
33     # Query all sled runs
34     q = select(
35         [page, categorylinks, revision, text],
36         (page.c.page_latest == revision.c.rev_id) & (text.c.old_id == revision.c.rev_text_id) &
37         (categorylinks.c.cl_from == page.c.page_id) & (categorylinks.c.cl_to == 'Rodelbahn'))
38     sledrun_pages = connection.execute(q)
39     # Original SQL:
40     # sql = u"select page_id, rev_id, old_id, page_title, old_text, 'In_Arbeit' in
41     # (select cl_to from categorylinks where cl_from=page_id) as under_construction
42     # from page, revision, text, categorylinks where page_latest=rev_id and old_id=rev_text_id and
43     # cl_from=page_id and cl_to='Rodelbahn' order by page_title"
44     
45     # Delete all existing entries in wrsledruncache
46     # We rely on transactions MySQL InnoDB
47     connection.execute(wrsledruncache.delete())
48     
49     # Refill wrsledruncache table
50     for sledrun_page in sledrun_pages:
51         try:
52             rodelbahnbox = wrvalidators.rodelbahnbox_from_str(sledrun_page.old_text)
53             sledrun = wrmwmarkup.sledrun_from_rodelbahnbox(rodelbahnbox, Sledrun())
54             sledrun.page_id = sledrun_page.page_id
55             sledrun.page_title = sledrun_page.page_title
56             sledrun.name_url = wrvalidators.sledrun_page_title_to_pretty_url(sledrun_page.page_title)
57             sledrun.under_construction = connection.execute(select(
58                 [sqlfunc.count()],
59                 (categorylinks.c.cl_from == sledrun_page.page_id) &
60                 (categorylinks.c.cl_to == 'In_Arbeit')).alias('x')).fetchone()[0] > 0
61             connection.execute(wrsledruncache.insert(sledrun.__dict__))
62         except ValueError as e:
63             transaction.rollback()
64             error_msg = f"Error at sled run '{sledrun_page.page_title}': {e}"
65             raise UpdateCacheError(error_msg, sledrun_page.page_title, e)
66     transaction.commit()
67
68
69 def update_wrinncache(connection):
70     """Updates the wrinncache table from the wiki. If convert errors occur, an UpdateCacheError exception
71     is raised. No other exception type should be raised under normal circumstances.
72     
73     >>> from sqlalchemy.engine import create_engine
74     >>> engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4')
75     >>> update_wrinncache(engine.connect())
76     """
77     metadata = schema.MetaData()
78     wrinncache = wrmwdb.wrinncache_table(metadata)
79     page = mwdb.page_table(metadata)
80     categorylinks = mwdb.categorylinks_table(metadata)
81     revision = mwdb.revision_table(metadata)
82     text = mwdb.text_table(metadata)
83
84     class Inn:
85         pass
86
87     transaction = connection.begin()
88
89     # Query all inns
90     q = select(
91         [page, categorylinks, revision, text],
92         (page.c.page_latest == revision.c.rev_id) & (text.c.old_id == revision.c.rev_text_id) &
93         (categorylinks.c.cl_from == page.c.page_id) & (categorylinks.c.cl_to == 'Gasthaus'))
94     inn_pages = connection.execute(q)
95         
96     # Delete all existing entries in wrinncache
97     # We rely on transactions MySQL InnoDB
98     connection.execute(wrinncache.delete())
99         
100     # Refill wrinncache table
101     for inn_page in inn_pages:
102         try:
103             gasthausbox = wrvalidators.gasthausbox_from_str(inn_page.old_text)
104             inn = wrmwmarkup.inn_from_gasthausbox(gasthausbox, Inn())
105             inn.page_id = inn_page.page_id
106             inn.page_title = inn_page.page_title
107             inn.under_construction = connection.execute(select(
108                 [sqlfunc.count()],
109                 (categorylinks.c.cl_from == inn_page.page_id) &
110                 (categorylinks.c.cl_to == 'In_Arbeit')).alias('x')) \
111                 .fetchone()[0] > 0  # it would be better to do this in the query above
112             connection.execute(wrinncache.insert(inn.__dict__))
113         except ValueError as e:
114             transaction.rollback()
115             error_msg = f"Error as inn '{inn_page.page_title}': {e}"
116             raise UpdateCacheError(error_msg, inn_page.page_title, e)
117     transaction.commit()
118
119
120 def update_wrreportcache(connection, page_id=None):
121     """Updates the wrreportcache table.
122     :param connection: sqlalchemy connection
123     :param page_id: Updates only the reportcache table for the sledrun described on the Winterrodeln wiki page
124         with the specified page_id. Use None for this parameter to update the whole table.
125
126     >>> from sqlalchemy.engine import create_engine
127     >>> engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4')
128     >>> update_wrreportcache(engine.connect())
129     """
130     metadata = schema.MetaData()
131     wrreportcache = wrmwdb.wrreportcache_table(metadata)
132     transaction = connection.begin()
133
134     # Delete the datasets we are going to update
135     sql_del = wrreportcache.delete()
136     if page_id is not None:
137         sql_del = sql_del.where(wrreportcache.c.page_id == page_id)
138     connection.execute(sql_del)
139
140     def insert_row(connection_, row_list_):
141         if len(row_list_) == 0:
142             return
143         # Insert the report
144         row_ = dict(row_list_[0])
145         connection_.execute(wrreportcache.insert(values=row_))
146
147     # Select the rows to update
148     sql = 'select page_id, page_title, wrreport.id as report_id, date_report, `condition`, description, author_name, ' \
149           'if(author_userid is null, null, author_username) as author_username from wrreport ' \
150           'where {0}`condition` is not null and date_invalid > now() and delete_date is null ' \
151           'order by page_id, date_report desc, date_entry desc' \
152           .format('' if page_id is None else f'page_id={page_id} and ')
153     cursor = connection.execute(sql)
154     page_id = None
155     row_list = []
156     for row in cursor:
157         if row.page_id != page_id:
158             insert_row(connection, row_list)
159             page_id = row.page_id
160             row_list = []
161         row_list.append(row)
162     insert_row(connection, row_list)
163     transaction.commit()
164
165
166 def update_wrmapcache(connection):
167     """Updates the wrmappointcache and wrmappathcache tables from the wiki. If convert errors occur,
168     an UpdateCacheError exception is raised. No other exception type should be raised under normal circumstances.
169     
170     >>> from sqlalchemy.engine import create_engine
171     >>> engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4')
172     >>> # or:
173     >>> # engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4&passwd=XXX')
174     >>> update_wrmapcache(engine.connect())
175     """
176     metadata = schema.MetaData()
177     page = mwdb.page_table(metadata)
178     categorylinks = mwdb.categorylinks_table(metadata)
179     revision = mwdb.revision_table(metadata)
180     text = mwdb.text_table(metadata)
181
182     transaction = connection.begin()
183
184     # Query all sledruns
185     q = select(
186         [page, categorylinks, revision, text],
187         (page.c.page_latest == revision.c.rev_id) & (text.c.old_id == revision.c.rev_text_id) &
188         (categorylinks.c.cl_from == page.c.page_id) & (categorylinks.c.cl_to == 'Rodelbahn'))
189     sledrun_pages = connection.execute(q)
190     # Original SQL:
191     # sql = u"select page_id, rev_id, old_id, page_title, old_text, 'In_Arbeit' in
192     # (select cl_to from categorylinks where cl_from=page_id) as under_construction
193     # from page, revision, text, categorylinks where page_latest=rev_id and old_id=rev_text_id and cl_from=page_id
194     # and cl_to='Rodelbahn' order by page_title"
195     
196     # Delete all existing entries in wrmappointcache
197     # We rely on transactions MySQL InnoDB
198     connection.execute('delete from wrmappointcache')
199     connection.execute('delete from wrmappathcache')
200     
201     # Refill wrmappointcache and wrmappathcache tables
202     for sledrun_page in sledrun_pages:
203         try:
204             import mwparserfromhell
205             wikicode = mwparserfromhell.parse(sledrun_page.old_text)
206             wrmap_list = wikicode.filter_tags(recursive=False, matches=lambda tag: tag.tag == 'wrmap')
207             if len(wrmap_list) == 0:
208                 continue  # not wrmap in page
209             if len(wrmap_list) > 1:
210                 raise UpdateCacheError(
211                     f'{len(wrmap_list)} <wrmap ...> entries found in article "{sledrun_page.page_title}"')
212             wrmap = wrmap_list[0]
213             geojson = wrmwmarkup.parse_wrmap(str(wrmap))
214
215             for feature in geojson['features']:
216                 properties = feature['properties']
217                 coordinates = feature['geometry']['coordinates']
218
219                 # Points
220                 if properties['type'] in wrmwmarkup.WRMAP_POINT_TYPES:
221                     lon, lat = coordinates
222                     label = properties.get('name')
223                     point_types = {
224                         'gasthaus': 'hut',
225                         'haltestelle': 'busstop',
226                         'parkplatz': 'carpark',
227                         'achtung': 'warning',
228                         'foto': 'photo',
229                         'verleih': 'rental',
230                         'punkt': 'point'
231                     }
232                     point_type = point_types[properties['type']]
233                     sql = 'insert into wrmappointcache (page_id, type, point, label) values (%s, %s, POINT(%s, %s), %s)'
234                     connection.execute(sql, (sledrun_page.page_id, point_type, lon, lat, label))
235
236                 # Paths
237                 elif properties['type'] in wrmwmarkup.WRMAP_LINE_TYPES:
238                     path_types = {
239                         'rodelbahn': 'sledrun',
240                         'gehweg': 'walkup',
241                         'alternative': 'alternative',
242                         'lift': 'lift',
243                         'anfahrt': 'recommendedcarroute',
244                         'linie': 'line'}
245                     path_type = path_types[properties['type']]
246                     path = ", ".join([f"{lon} {lat}" for lon, lat in coordinates])
247                     path = f'LineString({path})'
248                     if path_type == 'recommendedcarroute':
249                         continue
250                     sql = 'insert into wrmappathcache (path, page_id, type) values (GeomFromText(%s), %s, %s)'
251                     connection.execute(sql, (path, sledrun_page.page_id, path_type))
252
253                 else:
254                     raise RuntimeError(f'Unknown feature type {properties["type"]}')
255         except RuntimeError as e:
256             error_msg = f"Error at sledrun '{sledrun_page.page_title}': {e}"
257             transaction.rollback()
258             raise UpdateCacheError(error_msg, sledrun_page.page_title, e)
259     transaction.commit()
260
261
262 def update_wrregioncache(connection):
263     """Updates the wrregioncache table from the wiki.
264     It relays on the table wrsledruncache to be up-to-date.
265     No exceptions should be raised under normal circumstances.
266     
267     >>> from sqlalchemy.engine import create_engine
268     >>> engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4')
269     >>> # or:
270     >>> # engine = create_engine('mysql://philipp@localhost:3306/philipp_winterrodeln_wiki?charset=utf8mb4&passwd=XXX')
271     >>> update_wrregioncache(engine.connect())
272     """
273     metadata = schema.MetaData()
274     wrregion = wrmwdb.wrregion_table(metadata)
275     wrsledruncache = wrmwdb.wrsledruncache_table(metadata)
276     wrregioncache = wrmwdb.wrregioncache_table(metadata)
277
278     transaction = connection.begin()
279
280     # Delete all existing entries in wrregioncache
281     # We rely on transactions MySQL InnoDB
282     connection.execute(wrregioncache.delete())
283     
284     # Query all combinations of sledruns and regions
285     sel = select(
286         [
287             wrregion.c.id.label('region_id'),
288             sqlfunc.AsWKB(wrregion.c.border).label('border'),
289             wrsledruncache.c.page_id,
290             wrsledruncache.c.position_longitude,
291             wrsledruncache.c.position_latitude
292         ],
293         sqlfunc.contains(
294             wrregion.c.border,
295             sqlfunc.point(wrsledruncache.c.position_longitude, wrsledruncache.c.position_latitude)
296         )
297     )
298     ins = wrregioncache.insert()
299
300     # Refill wrregioncache
301     point = ogr.Geometry(ogr.wkbPoint)
302     result = connection.execute(sel)
303     for row in result:
304         point.SetPoint(0, row.position_longitude, row.position_latitude)
305         if point.Within(ogr.CreateGeometryFromWkb(row.border)):
306             connection.execute(ins.values(region_id=row.region_id, page_id=row.page_id))
307
308     # commit
309     transaction.commit()