PNA.fi koodi

food.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. #!/usr/bin/env python3
  2. # Copyright 2018 Toni Fadjukoff. All Rights Reserved.
  3. # Copyright 2019 Google LLC. All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. # The project is based on original rna.fi
  17. # project by Timo Siiranen before 2007-2010
  18. import sys
  19. day_names = [ "Maanantai", "Tiistai", "Keskiviikko", "Torstai",
  20. "Perjantai", "Lauantai", "Sunnuntai" ]
  21. import amica
  22. import sodexo
  23. import juvenes
  24. import campusravita
  25. allergies = [ "M", "L", "VL", "G", "K", "Ve" ]
  26. allergy_descriptions = {
  27. "M": "Maidoton",
  28. "L": "Laktoositon","VL": "Vähälaktoosinen",
  29. "G": "Gluteiiniton",
  30. "K": "Kasvis",
  31. "Ve": "Vegaani"
  32. }
  33. import os
  34. import time
  35. import datetime
  36. import re
  37. global_prefix = "";
  38. use_old = False; # 1 is good for testing, 0 for production system!
  39. unordered = []
  40. l = time.localtime()
  41. this_week = datetime.datetime.now().isocalendar()[1]
  42. updateException = None
  43. for restaurant_module in [sodexo, amica, juvenes, campusravita]:
  44. try:
  45. unordered += restaurant_module.get_restaurants(use_old, this_week)
  46. except Exception as e:
  47. updateException = e
  48. max_week = 0;
  49. for r in unordered:
  50. week = r[2]
  51. max_week = week if week > max_week or week == 1 else max_week
  52. if l[6] != 0 and this_week != max_week:
  53. # it's not sunday, don't force next week's menu yet
  54. max_week = this_week
  55. stamp = time.time() - 3600*24*7
  56. max_week_daterange = ""
  57. if max_week >= 1 and max_week <= 52:
  58. # figure out the date range
  59. while True:
  60. stamp_week = datetime.datetime.utcfromtimestamp(stamp).isocalendar()[1]
  61. if stamp_week == max_week:
  62. break
  63. stamp += 3600*24
  64. l1 = time.localtime(stamp)
  65. l2 = time.localtime(stamp + 3600*24*6)
  66. if l1[1] == l2[1]:
  67. # same month
  68. max_week_daterange = "{monday}-{sunday}.{month}.".format(monday=l1[2], sunday=l2[2], month=l1[1])
  69. else:
  70. # different months
  71. max_week_daterange = "{monday}.{month}.-{sunday}.{next_month}.".format(monday=l1[2], month=l1[1],
  72. sunday=l2[2], next_month=l2[1])
  73. max_week_daterange = " (" + str(max_week_daterange) + ")"
  74. file_header = '''<?xml version="1.0" encoding="utf-8"?>
  75. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  76. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fi" lang="fi">
  77. <head>
  78. <title>Ruokalistat</title>
  79. <meta charset="UTF-8"/>
  80. <link rel="stylesheet" type="text/css" href="{resources_prefix}ruoka.css" />
  81. </head>
  82. <body>
  83. <div id="notice" style="border: 1px solid black; border-radius: 5px; padding: 5px;">
  84. PNA.fi on kolmannen osapuolen tarjoama palvelu. En voi taata ruokalistojen oikeellisuutta. Virallisen ruokalistan saat näkyviin siirtymällä ravintolan omille sivuille painamalla sen nimestä. Jos huomaat ruokalistassa virheen, nopeiten virhe saadaan pois näkyvistä kun lähetät minulle siitä sähköpostia: <a href="mailto:lamperi+pna@gmail.com">lamperi+pna@gmail.com</a>
  85. </div>
  86. <form method="get" action="/cgi-bin/food.cgi">
  87. '''
  88. file_footer = """
  89. <div class="footer">Päivitetty {stamp}
  90. <input type="submit\" value=\"Päivitä nyt\" />
  91. / Palaute <a href=\"mailto:lamperi+pna@gmail.com\">lamperi+pna@gmail.com</a>
  92. / <a href="{{resources_prefix}}code.html\">Koodit täältä</a>
  93. / <a href="{{resources_prefix}}pna.html\">Mikä on PNA?</a>
  94. </div>
  95. </form>
  96. </body>
  97. </html>
  98. """.format(stamp=time.strftime("%d.%m.%Y %H:%M:%S"))
  99. def find_last_day_with_foods(restaurants):
  100. last_day = 0
  101. for r in restaurants:
  102. for day in range(7):
  103. if day in r[3]:
  104. last_day = day if day > last_day else last_day
  105. return last_day
  106. def write_days_header(fout, day, last_day):
  107. fout.write( " <span class=\"days\">")
  108. for i in range(last_day):
  109. if i == day:
  110. fout.write("{0} ".format(day_names[i]))
  111. else:
  112. fout.write("<a href=\"{0}.html\">{1}</a> ".format(i+1, day_names[i]))
  113. if day < 0:
  114. fout.write("Taulukko")
  115. else:
  116. fout.write("<a href=\"table.html\">Taulukko</a>")
  117. fout.write("</span>\n")
  118. def write_prefix_header(fout, prefix, day, resources_prefix):
  119. day = "table" if day == 0 else day
  120. fout.write("<span class=\"location\">")
  121. if prefix == "":
  122. fout.write("Kaikki ")
  123. else:
  124. fout.write("<a href=\"{resources_prefix}{day}.html\">Kaikki</a> ".format(resources_prefix=resources_prefix, day=day))
  125. if prefix == "tay/":
  126. fout.write("TaY ")
  127. else:
  128. fout.write("<a href=\"{resources_prefix}tay/{day}.html\">TaY</a> ".format(resources_prefix=resources_prefix, day=day))
  129. if prefix == "tays/":
  130. fout.write("TAYS ")
  131. else:
  132. fout.write("<a href=\"{resources_prefix}tays/{day}.html\">TAYS</a> ".format(resources_prefix=resources_prefix, day=day))
  133. if prefix == "tty/":
  134. fout.write("TTY ")
  135. else:
  136. fout.write("<a href=\"{resources_prefix}tty/{day}.html\">TTY</a> ".format(resources_prefix=resources_prefix, day=day))
  137. fout.write("</span>\n")
  138. def write_day(day, header, outfname, last_day, restaurants, prefix, resources_prefix):
  139. with open(outfname, "w", encoding="utf-8") as fout:
  140. fout.write(file_header.format(resources_prefix=resources_prefix))
  141. fout.write("<h1>{header}</h1>\n".format(header=header))
  142. # print weekday links
  143. fout.write("<div class=\"title\">\n")
  144. write_days_header(fout, day, last_day)
  145. fout.write(" <span class=\"allergy\">Näytä: ")
  146. for a in allergies:
  147. fout.write("<input type=\"checkbox\" name=\"allergy_{a}\" id=\"allergy_{a}\" onclick=\"highlight()\" />".format(a=a))
  148. fout.write("<span title=\"{allergy_description}\">{a}</span>".format(allergy_description=allergy_descriptions[a], a=a))
  149. fout.write("</span>\n")
  150. write_prefix_header(fout, prefix, day+1, resources_prefix);
  151. fout.write("</div>\n")
  152. # print foods
  153. foodnum = 0
  154. eatable_food_numbers = {}
  155. maybe_eatable_food_numbers = {}
  156. for a in allergies:
  157. eatable_food_numbers[a] = []
  158. maybe_eatable_food_numbers[a] = []
  159. css_class = "left"
  160. fout.write("<div class=\"foods\"><div class=\"{css_class}\">\n".format(css_class=css_class))
  161. for r in restaurants:
  162. title, open_hours, week, week_foods, info = r[:5]
  163. exception = r[5].replace("\n", "<br>") if len(r) > 5 and r[5] else "Ruokalistaa ei saatavilla."
  164. title2, url, lazy_allergies, info_class = info[0:4]
  165. if day in week_foods or day < 5:
  166. if info_class != css_class:
  167. css_class = info_class
  168. fout.write("</div><div class=\"{css_class}\">\n".format(css_class=css_class))
  169. url = url.replace("&", "&amp;")
  170. fout.write("<h2><a href=\"{url}\">{title}</a></h2>\n".format(url=url, title=title))
  171. if not day in week_foods:
  172. fout.write("<p class=\"missing\">{exception}</p>".format(exception=exception))
  173. continue
  174. if week != "" and week != max_week:
  175. if week > max_week or (week == 1 and max_week == 52):
  176. # early..
  177. fout.write("<p class=\"nextweek\">Viikon {week} ruokalista:</p>".format(week=week))
  178. else:
  179. fout.write("<p class=\"missing\">Saatavilla vain viikon {week} ruokalista.</p>".format(week=week))
  180. continue
  181. if len(week_foods[day]) == 0:
  182. fout.write("<p class=\"missing\">Ei ruokatietoja päivälle.</p>")
  183. continue
  184. fout.write("<ul class=\"food\">\n")
  185. for food in week_foods[day]:
  186. output = []
  187. total_allergies = {}
  188. maybe_allergies = {}
  189. for a in allergies:
  190. total_allergies[a] = 0
  191. maybe_allergies[a] = 0
  192. part_count = 0
  193. for part in food.split("\n"):
  194. # who cares?
  195. if re.match("Peruna|Riisi", part):
  196. continue
  197. # fries: well, maybe we do care, but we don't care about allergy stuff
  198. # and keep it in the same line as the previous food so as not to
  199. # waste visible space
  200. (part, fries) = re.subn("Tikkuperunat|Ranskalaiset perunat", "", part)
  201. fries = fries > 0
  202. part_count += 1
  203. # add missing () around allergies
  204. part = re.sub(" (([MLGKA]|VL|Ve|VE|Veg|Hot)(, *([MLGKA]|VL|Ve|VE|Veg|Hot|))+)$", " (\\1)", part)
  205. match = re.match("^(.*) \\(([^\\)]+)\\)$", part)
  206. if match:
  207. # fix allergy issues
  208. food = match.group(1)
  209. allergy = match.group(2)
  210. # standardization
  211. allergy = re.sub("Kasvis", "K", allergy)
  212. allergy = re.sub("([MLGK]|VL)([MLGK]|[VL])", "\\1,\\2", allergy)
  213. # spaces to commas
  214. allergy = re.sub("saatavana[: ]+(.*)$", "eriks: \\1", allergy)
  215. allergy = re.sub(" +", ",", allergy)
  216. # remove double commas
  217. allergy = re.sub(",+", ",", allergy)
  218. # eriks: standardization
  219. allergy = re.sub(",?eriks:,?", ", eriks: ", allergy)
  220. # remove extra commas/spaces from beginning/end
  221. allergy = re.sub("^[, ]+", "", allergy)
  222. allergy = re.sub("[, ]+$", "", allergy)
  223. part = "{food} ({allergy})".format(food=food, allergy=allergy)
  224. if output and not fries:
  225. output.append("<br />\n")
  226. match = re.search("Saatavana myös: (.*)", part)
  227. if match:
  228. alt = match.group(1)
  229. alt = re.sub(r"^\((.*)\)$", r"\1", alt)
  230. alt = re.sub("[, ]+", r",", alt)
  231. alt = re.sub("^,+", "", alt)
  232. alt = re.sub(",+$", "", alt)
  233. part = re.sub(r"\)[- ]*Saatavana myös:.*", "eriks: {alt})".format(alt=alt), part)
  234. part = re.sub(r"[- ]*Saatavana myös:.*", "(eriks: {alt})".format(alt=alt), part)
  235. match = re.match(r"^(.*)(\([^\)]+\))$", part)
  236. if match:
  237. text = match.group(1)
  238. allergy = match.group(2)
  239. if fries:
  240. output.append(", {text}".format(text=text))
  241. else:
  242. output.append("{text} <span class=\"allergy\">{allergy}</span>".format(text=text, allergy=allergy))
  243. allergy = re.sub(r"^\((.*)\)$", r"\1", allergy)
  244. allergy = re.sub(" *eriks: ", "", allergy)
  245. this_allergies = set()
  246. for a in re.split("[, ]", allergy):
  247. for al in allergies:
  248. if a == al:
  249. this_allergies.add(a)
  250. break
  251. if "L" in this_allergies:
  252. this_allergies.add("VL")
  253. for a in this_allergies:
  254. if a in total_allergies:
  255. total_allergies[a] += 1
  256. if a in maybe_allergies:
  257. maybe_allergies[a] += 1
  258. match = re.search("M", lazy_allergies)
  259. if match:
  260. if "L" in this_allergies and not "M" in this_allergies:
  261. maybe_allergies["M"] += 1
  262. else:
  263. if lazy_allergies == "all":
  264. for a in allergies:
  265. maybe_allergies[a] += 1
  266. output.append(part)
  267. allergy_output = ""
  268. for a in allergies:
  269. if total_allergies[a] == part_count:
  270. eatable_food_numbers[a].append(foodnum)
  271. elif maybe_allergies[a]== part_count:
  272. maybe_eatable_food_numbers[a].append(foodnum)
  273. fout.write(" <li id=\"f{foodnum}\">{output}</li>\n".format(foodnum=foodnum, output="".join(output)))
  274. foodnum += 1
  275. fout.write("</ul>\n")
  276. # write allergy scripts
  277. fout.write('<script type="text/javascript" src="{resources_prefix}ruoka.js"></script>'.format(resources_prefix=resources_prefix))
  278. fout.write('<script type="text/javascript">\n')
  279. fout.write("var eatable_foods = [];\n")
  280. fout.write("var maybe_eatable_foods = [];\n")
  281. for a in allergies:
  282. fout.write("eatable_foods[\"{a}\"] = [{eatable_food_number}];\n".format(a=a, eatable_food_number=",".join(str(e) for e in eatable_food_numbers[a])))
  283. fout.write("maybe_eatable_foods[\"{a}\"] = [{maybe_eatable_food_number}];\n".format(a=a, maybe_eatable_food_number=",".join(str(e) for e in maybe_eatable_food_numbers[a])))
  284. allergy_string = ",".join('"{a}"'.format(a=a) for a in allergies)
  285. fout.write("var allergies = [{allergies}];\n".format(allergies = allergy_string))
  286. fout.write("var food_count = {foodnum};\n".format(foodnum = foodnum))
  287. fout.write("window.onload = function() { set_allergies(); show_warning(); };\n")
  288. fout.write("</script>\n")
  289. fout.write("</div></div>{file_footer}".format(file_footer=file_footer.format(resources_prefix=resources_prefix)))
  290. def write_all_days(restaurants, prefix, title, resources_prefix):
  291. try:
  292. os.mkdir(prefix)
  293. except OSError as err:
  294. pass # hope it already exists
  295. last_day = find_last_day_with_foods(restaurants);
  296. for day in range(7):
  297. outfname = "{prefix}{day}.html".format(prefix=prefix, day=day+1)
  298. if day > last_day:
  299. try:
  300. os.unlink(outfname)
  301. except OSError as err:
  302. pass # probably did not exist
  303. continue
  304. header = "{day_name} - {title} vko {max_week}{max_week_daterange}".format(day_name=day_names[day], title=title,
  305. max_week=max_week, max_week_daterange=max_week_daterange)
  306. write_day(day, header, outfname, last_day, restaurants, prefix, resources_prefix)
  307. def write_table(restaurants, prefix, title, resources_prefix):
  308. last_day = find_last_day_with_foods(restaurants);
  309. outfname = "{prefix}table.html".format(prefix=prefix)
  310. with open(outfname, "w", encoding="utf-8") as fout:
  311. header = "{title} vko {max_week}{max_week_daterange}".format(title=title, max_week=max_week, max_week_daterange=max_week_daterange)
  312. fout.write(file_header.format(resources_prefix=resources_prefix))
  313. fout.write("<h1>{header}</h1>\n".format(header=header))
  314. fout.write("<div class=\"title\">\n")
  315. write_days_header(fout, -1, last_day)
  316. write_prefix_header(fout, prefix, 0, resources_prefix)
  317. fout.write("</div><table border=\"1\"><tr><th>Päivä</th>")
  318. for r in restaurants:
  319. (title, open_hours, week, week_foods, info) = r[:5]
  320. (title2, url) = info[0:2]
  321. url = re.sub("&", "&nbsp;", url)
  322. fout.write("<th><a href=\"{url}\">{title}</a></th>".format(url=url, title=title))
  323. fout.write("</tr>\n")
  324. for day in range(last_day):
  325. fout.write("<tr><td>{day_name}</td>\n".format(day_name=day_names[day]))
  326. for r in restaurants:
  327. (title, open_hours, week, week_foods, info) = r[:5]
  328. if day in week_foods and (week == "" or week == max_week):
  329. fout.write("<td><ul>\n")
  330. for food in week_foods[day]:
  331. fout.write("<li>{food}</li>".format(food=food))
  332. fout.write("</ul></td>\n")
  333. else:
  334. fout.write("<td></td>\n")
  335. fout.write("</tr\n")
  336. fout.write("</table>{file_footer}".format(file_footer=file_footer.format(resources_prefix=resources_prefix)))
  337. def get_restaurants_sorted(restaurants):
  338. # consider writing comparator
  339. out = []
  340. for r in restaurants:
  341. if r[4][3] == "left":
  342. out.append(r)
  343. for r in restaurants:
  344. if r[4][3] == "right":
  345. out.append(r)
  346. for r in restaurants:
  347. if r[4][3] == "middle" and not re.search("TAMK", r[4][0]):
  348. out.append(r)
  349. for r in restaurants:
  350. if r[4][3] == "middle" and re.search("TAMK", r[4][0]):
  351. out.append(r)
  352. return out
  353. def get_restaurants_with_prefix(prefix, restaurants):
  354. out = []
  355. for r in restaurants:
  356. if re.search(prefix, r[4][0]):
  357. out.append(r)
  358. return get_restaurants_sorted(out)
  359. tty_title = "TTY:n ruokalistat"
  360. tty = get_restaurants_with_prefix("TTY", unordered)
  361. write_all_days(tty, "tty/", tty_title, "../")
  362. write_table(tty, "tty/", tty_title, "../")
  363. tay_title = "Tampereen yliopiston ruokalistat"
  364. tay = get_restaurants_with_prefix("TaY", unordered)
  365. write_all_days(tay, "tay/", tay_title, "../")
  366. write_table(tay, "tay/", tay_title, "../")
  367. tays_title = "TAYS:n ruokalistat"
  368. tays = get_restaurants_with_prefix("TAYS", unordered)
  369. write_all_days(tays, "tays/", tays_title, "../")
  370. write_table(tays, "tays/", tays_title, "../")
  371. for r in unordered:
  372. if re.search(r"^\(TaY\)", r[0]):
  373. r[4][3] = "left"
  374. if re.search(r"^\(TTY\)", r[0]):
  375. r[4][3] = "right"
  376. if re.search(r"^\(TAYS\)", r[0]):
  377. r[4][3] = "middle"
  378. all_title = "Tampereen yliopistojen ruokalistat";
  379. all_restaurants = get_restaurants_sorted(unordered);
  380. # move fusion kitchen last
  381. #fusion = splice(@all_restaurants, 1, 1);
  382. #splice(@all_restaurants, 4, 0, @fusion);
  383. write_all_days(all_restaurants, "", all_title, "");
  384. write_table(all_restaurants, "", all_title, "");
  385. if updateException is not None:
  386. print("Exception happened while fetching menus")
  387. raise updateException