PNA.fi koodi

food.py 18KB

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