fnschool 20250109.80500.803__py3-none-any.whl → 20251011.80531.840__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 fnschool might be problematic. Click here for more details.

Files changed (276) hide show
  1. fnschoo1/__init__.py +52 -0
  2. fnschoo1/canteen/admin.py +3 -0
  3. fnschoo1/canteen/apps.py +6 -0
  4. fnschoo1/canteen/forms.py +84 -0
  5. fnschoo1/canteen/migrations/0001_initial.py +119 -0
  6. fnschoo1/canteen/migrations/0002_ingredient_is_disabled.py +20 -0
  7. fnschoo1/canteen/migrations/0003_consumption_is_disabled_alter_ingredient_is_disabled.py +23 -0
  8. fnschoo1/canteen/migrations/0004_alter_ingredient_name_category_and_more.py +66 -0
  9. fnschoo1/canteen/migrations/0005_alter_category_created_at_alter_category_name_and_more.py +41 -0
  10. fnschoo1/canteen/migrations/0006_category_is_disabled_alter_category_user_and_more.py +49 -0
  11. fnschoo1/canteen/migrations/0007_alter_consumption_amount_used_and_more.py +30 -0
  12. fnschoo1/canteen/migrations/0008_category_abbreviation_mealtype.py +67 -0
  13. fnschoo1/canteen/migrations/0009_alter_category_abbreviation_and_more.py +55 -0
  14. fnschoo1/canteen/migrations/0010_alter_consumption_options_alter_ingredient_options_and_more.py +215 -0
  15. fnschoo1/canteen/migrations/0011_category_pin_to_consumptions_top.py +23 -0
  16. fnschoo1/canteen/migrations/0012_alter_ingredient_storage_date.py +18 -0
  17. fnschoo1/canteen/models.py +179 -0
  18. fnschoo1/canteen/templates/canteen/category/create.html +17 -0
  19. fnschoo1/canteen/templates/canteen/category/delete.html +61 -0
  20. fnschoo1/canteen/templates/canteen/category/list.html +63 -0
  21. fnschoo1/canteen/templates/canteen/category/update.html +23 -0
  22. fnschoo1/canteen/templates/canteen/close.html +11 -0
  23. fnschoo1/canteen/templates/canteen/consumption/_create.html +19 -0
  24. fnschoo1/canteen/templates/canteen/consumption/create.html +456 -0
  25. fnschoo1/canteen/templates/canteen/ingredient/close.html +11 -0
  26. fnschoo1/canteen/templates/canteen/ingredient/create.html +19 -0
  27. fnschoo1/canteen/templates/canteen/ingredient/create_one.html +17 -0
  28. fnschoo1/canteen/templates/canteen/ingredient/delete.html +41 -0
  29. fnschoo1/canteen/templates/canteen/ingredient/list.html +128 -0
  30. fnschoo1/canteen/templates/canteen/ingredient/update.html +23 -0
  31. fnschoo1/canteen/templates/canteen/meal_type/create.html +17 -0
  32. fnschoo1/canteen/templates/canteen/meal_type/delete.html +56 -0
  33. fnschoo1/canteen/templates/canteen/meal_type/list.html +59 -0
  34. fnschoo1/canteen/templates/canteen/meal_type/update.html +23 -0
  35. fnschoo1/canteen/tests.py +3 -0
  36. fnschoo1/canteen/urls.py +116 -0
  37. fnschoo1/canteen/views.py +814 -0
  38. fnschoo1/canteen/workbook/generate.py +2098 -0
  39. fnschoo1/db.sqlite3 +0 -0
  40. fnschoo1/fnschool/__init__.py +13 -0
  41. fnschoo1/fnschool/asgi.py +16 -0
  42. fnschoo1/fnschool/settings.py +167 -0
  43. fnschoo1/fnschool/templatetags/fnschool_tags.py +27 -0
  44. fnschoo1/fnschool/urls.py +30 -0
  45. fnschoo1/fnschool/views.py +9 -0
  46. fnschoo1/fnschool/wsgi.py +16 -0
  47. fnschoo1/locale/en/LC_MESSAGES/django.mo +0 -0
  48. fnschoo1/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  49. fnschoo1/manage.py +25 -0
  50. fnschoo1/profiles/admin.py +27 -0
  51. fnschoo1/profiles/apps.py +12 -0
  52. fnschoo1/profiles/forms.py +67 -0
  53. fnschoo1/profiles/migrations/0001_initial.py +192 -0
  54. fnschoo1/profiles/migrations/0002_alter_profile_bio.py +20 -0
  55. fnschoo1/profiles/migrations/0003_alter_profile_options_alter_profile_address_and_more.py +67 -0
  56. fnschoo1/profiles/migrations/0004_profile_gender.py +26 -0
  57. fnschoo1/profiles/migrations/0005_alter_profile_gender.py +23 -0
  58. fnschoo1/profiles/models.py +60 -0
  59. fnschoo1/profiles/signals.py +20 -0
  60. fnschoo1/profiles/templates/profiles/create.html +16 -0
  61. fnschoo1/profiles/templates/profiles/detail.html +14 -0
  62. fnschoo1/profiles/templates/profiles/edit.html +12 -0
  63. fnschoo1/profiles/templates/profiles/log_in.html +20 -0
  64. fnschoo1/profiles/templates/profiles/log_out.html +12 -0
  65. fnschoo1/profiles/tests.py +3 -0
  66. fnschoo1/profiles/urls.py +15 -0
  67. fnschoo1/profiles/views.py +63 -0
  68. fnschoo1/static/css/bootstrap.min.css +11776 -0
  69. fnschoo1/static/css/fnschool.css +26 -0
  70. fnschoo1/static/js/bootstrap.bundle.min.js +4223 -0
  71. fnschoo1/static/js/bootstrap.min.js +2919 -0
  72. fnschoo1/static/js/fnschool.js +84 -0
  73. fnschoo1/static/js/jquery.min.js +5413 -0
  74. fnschoo1/static/js/jquery.slim.min.js +4331 -0
  75. fnschoo1/static/js/popper.min.js +1306 -0
  76. fnschoo1/static_collected/admin/css/autocomplete.css +377 -0
  77. fnschoo1/static_collected/admin/css/base.css +1224 -0
  78. fnschoo1/static_collected/admin/css/changelists.css +334 -0
  79. fnschoo1/static_collected/admin/css/dark_mode.css +136 -0
  80. fnschoo1/static_collected/admin/css/dashboard.css +30 -0
  81. fnschoo1/static_collected/admin/css/forms.css +546 -0
  82. fnschoo1/static_collected/admin/css/login.css +62 -0
  83. fnschoo1/static_collected/admin/css/nav_sidebar.css +145 -0
  84. fnschoo1/static_collected/admin/css/responsive.css +1043 -0
  85. fnschoo1/static_collected/admin/css/responsive_rtl.css +84 -0
  86. fnschoo1/static_collected/admin/css/rtl.css +311 -0
  87. fnschoo1/static_collected/admin/css/vendor/select2/select2.css +708 -0
  88. fnschoo1/static_collected/admin/css/vendor/select2/select2.min.css +1 -0
  89. fnschoo1/static_collected/admin/css/widgets.css +639 -0
  90. fnschoo1/static_collected/admin/js/SelectBox.js +128 -0
  91. fnschoo1/static_collected/admin/js/SelectFilter2.js +503 -0
  92. fnschoo1/static_collected/admin/js/actions.js +232 -0
  93. fnschoo1/static_collected/admin/js/admin/DateTimeShortcuts.js +496 -0
  94. fnschoo1/static_collected/admin/js/admin/RelatedObjectLookups.js +276 -0
  95. fnschoo1/static_collected/admin/js/autocomplete.js +33 -0
  96. fnschoo1/static_collected/admin/js/calendar.js +251 -0
  97. fnschoo1/static_collected/admin/js/cancel.js +29 -0
  98. fnschoo1/static_collected/admin/js/change_form.js +21 -0
  99. fnschoo1/static_collected/admin/js/collapse.js +43 -0
  100. fnschoo1/static_collected/admin/js/core.js +174 -0
  101. fnschoo1/static_collected/admin/js/filters.js +37 -0
  102. fnschoo1/static_collected/admin/js/inlines.js +439 -0
  103. fnschoo1/static_collected/admin/js/jquery.init.js +8 -0
  104. fnschoo1/static_collected/admin/js/nav_sidebar.js +81 -0
  105. fnschoo1/static_collected/admin/js/popup_response.js +24 -0
  106. fnschoo1/static_collected/admin/js/prepopulate.js +43 -0
  107. fnschoo1/static_collected/admin/js/prepopulate_init.js +20 -0
  108. fnschoo1/static_collected/admin/js/theme.js +57 -0
  109. fnschoo1/static_collected/admin/js/urlify.js +529 -0
  110. fnschoo1/static_collected/admin/js/vendor/jquery/jquery.js +10913 -0
  111. fnschoo1/static_collected/admin/js/vendor/jquery/jquery.min.js +2 -0
  112. fnschoo1/static_collected/admin/js/vendor/select2/i18n/af.js +43 -0
  113. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ar.js +36 -0
  114. fnschoo1/static_collected/admin/js/vendor/select2/i18n/az.js +33 -0
  115. fnschoo1/static_collected/admin/js/vendor/select2/i18n/bg.js +38 -0
  116. fnschoo1/static_collected/admin/js/vendor/select2/i18n/bn.js +39 -0
  117. fnschoo1/static_collected/admin/js/vendor/select2/i18n/bs.js +48 -0
  118. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ca.js +41 -0
  119. fnschoo1/static_collected/admin/js/vendor/select2/i18n/cs.js +62 -0
  120. fnschoo1/static_collected/admin/js/vendor/select2/i18n/da.js +37 -0
  121. fnschoo1/static_collected/admin/js/vendor/select2/i18n/de.js +41 -0
  122. fnschoo1/static_collected/admin/js/vendor/select2/i18n/dsb.js +51 -0
  123. fnschoo1/static_collected/admin/js/vendor/select2/i18n/el.js +43 -0
  124. fnschoo1/static_collected/admin/js/vendor/select2/i18n/en.js +41 -0
  125. fnschoo1/static_collected/admin/js/vendor/select2/i18n/es.js +41 -0
  126. fnschoo1/static_collected/admin/js/vendor/select2/i18n/et.js +38 -0
  127. fnschoo1/static_collected/admin/js/vendor/select2/i18n/eu.js +45 -0
  128. fnschoo1/static_collected/admin/js/vendor/select2/i18n/fa.js +42 -0
  129. fnschoo1/static_collected/admin/js/vendor/select2/i18n/fi.js +42 -0
  130. fnschoo1/static_collected/admin/js/vendor/select2/i18n/fr.js +43 -0
  131. fnschoo1/static_collected/admin/js/vendor/select2/i18n/gl.js +40 -0
  132. fnschoo1/static_collected/admin/js/vendor/select2/i18n/he.js +44 -0
  133. fnschoo1/static_collected/admin/js/vendor/select2/i18n/hi.js +40 -0
  134. fnschoo1/static_collected/admin/js/vendor/select2/i18n/hr.js +45 -0
  135. fnschoo1/static_collected/admin/js/vendor/select2/i18n/hsb.js +51 -0
  136. fnschoo1/static_collected/admin/js/vendor/select2/i18n/hu.js +44 -0
  137. fnschoo1/static_collected/admin/js/vendor/select2/i18n/hy.js +40 -0
  138. fnschoo1/static_collected/admin/js/vendor/select2/i18n/id.js +36 -0
  139. fnschoo1/static_collected/admin/js/vendor/select2/i18n/is.js +37 -0
  140. fnschoo1/static_collected/admin/js/vendor/select2/i18n/it.js +43 -0
  141. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ja.js +40 -0
  142. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ka.js +42 -0
  143. fnschoo1/static_collected/admin/js/vendor/select2/i18n/km.js +38 -0
  144. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ko.js +42 -0
  145. fnschoo1/static_collected/admin/js/vendor/select2/i18n/lt.js +45 -0
  146. fnschoo1/static_collected/admin/js/vendor/select2/i18n/lv.js +41 -0
  147. fnschoo1/static_collected/admin/js/vendor/select2/i18n/mk.js +42 -0
  148. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ms.js +38 -0
  149. fnschoo1/static_collected/admin/js/vendor/select2/i18n/nb.js +38 -0
  150. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ne.js +44 -0
  151. fnschoo1/static_collected/admin/js/vendor/select2/i18n/nl.js +46 -0
  152. fnschoo1/static_collected/admin/js/vendor/select2/i18n/pl.js +43 -0
  153. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ps.js +41 -0
  154. fnschoo1/static_collected/admin/js/vendor/select2/i18n/pt-BR.js +39 -0
  155. fnschoo1/static_collected/admin/js/vendor/select2/i18n/pt.js +41 -0
  156. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ro.js +43 -0
  157. fnschoo1/static_collected/admin/js/vendor/select2/i18n/ru.js +48 -0
  158. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sk.js +61 -0
  159. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sl.js +41 -0
  160. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sq.js +43 -0
  161. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sr-Cyrl.js +48 -0
  162. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sr.js +48 -0
  163. fnschoo1/static_collected/admin/js/vendor/select2/i18n/sv.js +40 -0
  164. fnschoo1/static_collected/admin/js/vendor/select2/i18n/th.js +36 -0
  165. fnschoo1/static_collected/admin/js/vendor/select2/i18n/tk.js +36 -0
  166. fnschoo1/static_collected/admin/js/vendor/select2/i18n/tr.js +40 -0
  167. fnschoo1/static_collected/admin/js/vendor/select2/i18n/uk.js +59 -0
  168. fnschoo1/static_collected/admin/js/vendor/select2/i18n/vi.js +37 -0
  169. fnschoo1/static_collected/admin/js/vendor/select2/i18n/zh-CN.js +36 -0
  170. fnschoo1/static_collected/admin/js/vendor/select2/i18n/zh-TW.js +33 -0
  171. fnschoo1/static_collected/admin/js/vendor/select2/select2.full.js +7115 -0
  172. fnschoo1/static_collected/admin/js/vendor/select2/select2.full.min.js +2 -0
  173. fnschoo1/static_collected/admin/js/vendor/xregexp/xregexp.js +4993 -0
  174. fnschoo1/static_collected/admin/js/vendor/xregexp/xregexp.min.js +160 -0
  175. fnschoo1/static_collected/css/bootstrap.min.css +11776 -0
  176. fnschoo1/static_collected/css/fnschool.css +26 -0
  177. fnschoo1/static_collected/js/bootstrap.bundle.min.js +4223 -0
  178. fnschoo1/static_collected/js/bootstrap.min.js +2919 -0
  179. fnschoo1/static_collected/js/fnschool.js +84 -0
  180. fnschoo1/static_collected/js/jquery.min.js +5413 -0
  181. fnschoo1/static_collected/js/jquery.slim.min.js +4331 -0
  182. fnschoo1/static_collected/js/popper.min.js +1306 -0
  183. fnschoo1/templates/base/_css.html +1 -0
  184. fnschoo1/templates/base/_js.html +15 -0
  185. fnschoo1/templates/base/content.html +30 -0
  186. fnschoo1/templates/base/header_content_footer.html +35 -0
  187. fnschoo1/templates/close.html +11 -0
  188. fnschoo1/templates/home.html +51 -0
  189. fnschoo1/templates/includes/_footer.html +39 -0
  190. fnschoo1/templates/includes/_header.html +77 -0
  191. fnschoo1/templates/includes/_navigation.html +0 -0
  192. fnschoo1/templates/includes/_paginator.html +27 -0
  193. fnschoo1/templates/registration/logged_out.html +0 -0
  194. fnschoo1/templates/registration/login.html +0 -0
  195. fnschool-20251011.80531.840.dist-info/METADATA +179 -0
  196. fnschool-20251011.80531.840.dist-info/RECORD +207 -0
  197. {fnschool-20250109.80500.803.dist-info → fnschool-20251011.80531.840.dist-info}/WHEEL +1 -1
  198. fnschool-20251011.80531.840.dist-info/entry_points.txt +2 -0
  199. fnschool-20251011.80531.840.dist-info/top_level.txt +1 -0
  200. fnschool/__init__.py +0 -35
  201. fnschool/__main__.py +0 -16
  202. fnschool/app.py +0 -103
  203. fnschool/canteen/__init__.py +0 -3
  204. fnschool/canteen/__main__.py +0 -3
  205. fnschool/canteen/bill.py +0 -253
  206. fnschool/canteen/canteen.py +0 -1
  207. fnschool/canteen/canteen.toml +0 -61
  208. fnschool/canteen/config.py +0 -10
  209. fnschool/canteen/consuming.py +0 -53
  210. fnschool/canteen/currency.py +0 -17
  211. fnschool/canteen/data/bill.i18n.xlsx +0 -0
  212. fnschool/canteen/data/bill.xlsx +0 -0
  213. fnschool/canteen/data/consuming.xlsx +0 -0
  214. fnschool/canteen/data/purchase_list.xlsx +0 -0
  215. fnschool/canteen/entry.py +0 -40
  216. fnschool/canteen/food.py +0 -206
  217. fnschool/canteen/food_classes.py +0 -33
  218. fnschool/canteen/food_classes.toml +0 -64
  219. fnschool/canteen/operator.py +0 -91
  220. fnschool/canteen/path.py +0 -28
  221. fnschool/canteen/spreadsheet/base.py +0 -212
  222. fnschool/canteen/spreadsheet/consuming.py +0 -310
  223. fnschool/canteen/spreadsheet/consumingsum.py +0 -76
  224. fnschool/canteen/spreadsheet/cover.py +0 -64
  225. fnschool/canteen/spreadsheet/ctspreadsheet.py +0 -351
  226. fnschool/canteen/spreadsheet/food.py +0 -680
  227. fnschool/canteen/spreadsheet/inventory.py +0 -375
  228. fnschool/canteen/spreadsheet/merging.py +0 -340
  229. fnschool/canteen/spreadsheet/preconsuming.py +0 -329
  230. fnschool/canteen/spreadsheet/purchasing.py +0 -885
  231. fnschool/canteen/spreadsheet/purchasingsum.py +0 -110
  232. fnschool/canteen/spreadsheet/spreadsheet.py +0 -363
  233. fnschool/canteen/spreadsheet/translating.py +0 -12
  234. fnschool/canteen/spreadsheet/unwarehousing.py +0 -178
  235. fnschool/canteen/spreadsheet/unwarehousingsum.py +0 -11
  236. fnschool/canteen/spreadsheet/warehousing.py +0 -360
  237. fnschool/canteen/spreadsheet/workbook.py +0 -17
  238. fnschool/canteen/test.py +0 -97
  239. fnschool/config.py +0 -48
  240. fnschool/entry.py +0 -67
  241. fnschool/exam/__init__.py +0 -8
  242. fnschool/exam/data/parental_emails.xlsx +0 -0
  243. fnschool/exam/data/score.xlsx +0 -0
  244. fnschool/exam/email.py +0 -349
  245. fnschool/exam/entry.py +0 -36
  246. fnschool/exam/language.py +0 -19
  247. fnschool/exam/path.py +0 -24
  248. fnschool/exam/score.py +0 -1191
  249. fnschool/exam/subject.py +0 -20
  250. fnschool/exam/teacher.py +0 -54
  251. fnschool/external.py +0 -89
  252. fnschool/games/__init__.py +0 -1
  253. fnschool/games/__main__.py +0 -1
  254. fnschool/games/games.py +0 -1
  255. fnschool/inoutput.py +0 -97
  256. fnschool/language.py +0 -40
  257. fnschool/locales/en_US/LC_MESSAGES/fnschool.mo +0 -0
  258. fnschool/locales/zh_CN/LC_MESSAGES/fnschool.mo +0 -0
  259. fnschool/locales/zh_HK/LC_MESSAGES/fnschool.mo +0 -0
  260. fnschool/locales/zh_SG/LC_MESSAGES/fnschool.mo +0 -0
  261. fnschool/locales/zh_TW/LC_MESSAGES/fnschool.mo +0 -0
  262. fnschool/path.py +0 -45
  263. fnschool/test.py +0 -24
  264. fnschool/user.py +0 -262
  265. fnschool-20250109.80500.803.dist-info/METADATA +0 -342
  266. fnschool-20250109.80500.803.dist-info/RECORD +0 -78
  267. fnschool-20250109.80500.803.dist-info/entry_points.txt +0 -5
  268. fnschool-20250109.80500.803.dist-info/top_level.txt +0 -1
  269. /fnschool/canteen/consume.py → /fnschoo1/canteen/__init__.py +0 -0
  270. /fnschool/canteen/food_recount.toml → /fnschoo1/canteen/migrations/__init__.py +0 -0
  271. {fnschool/canteen/spreadsheet → fnschoo1/canteen/workbook}/__init__.py +0 -0
  272. /fnschool/exam/__main__.py → /fnschoo1/fnschool/templatetags/__init__.py +0 -0
  273. /fnschool/canteen/food_recounts.toml → /fnschoo1/profiles/__init__.py +0 -0
  274. /fnschool/canteen/warehouse.py → /fnschoo1/profiles/migrations/__init__.py +0 -0
  275. /fnschool/canteen/workbook.toml → /fnschoo1/templates/base/_content.html +0 -0
  276. {fnschool-20250109.80500.803.dist-info → fnschool-20251011.80531.840.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,814 @@
1
+ import io
2
+ import re
3
+ from datetime import date, datetime
4
+ from decimal import (
5
+ ROUND_DOWN,
6
+ ROUND_FLOOR,
7
+ ROUND_HALF_UP,
8
+ Decimal,
9
+ getcontext,
10
+ localcontext,
11
+ )
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+ from dateutil import parser as date_parser
17
+ from dateutil.relativedelta import relativedelta
18
+ from django.conf import settings
19
+ from django.contrib.auth.decorators import login_required
20
+ from django.contrib.auth.mixins import LoginRequiredMixin
21
+ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
22
+ from django.db.models import (
23
+ DecimalField,
24
+ ExpressionWrapper,
25
+ F,
26
+ IntegerField,
27
+ Q,
28
+ Sum,
29
+ Value,
30
+ )
31
+ from django.db.models.functions import Coalesce
32
+ from django.http import HttpResponse
33
+ from django.shortcuts import get_object_or_404, redirect, render
34
+ from django.urls import reverse_lazy
35
+ from django.utils.encoding import escape_uri_path
36
+ from django.views.decorators.http import require_POST
37
+ from django.views.generic import (
38
+ CreateView,
39
+ DeleteView,
40
+ DetailView,
41
+ ListView,
42
+ UpdateView,
43
+ )
44
+ from fnschool import _, count_chinese_characters
45
+ from openpyxl import Workbook
46
+ from openpyxl.comments import Comment
47
+ from openpyxl.styles import Alignment, Font
48
+ from openpyxl.utils import get_column_letter
49
+
50
+ from .forms import (
51
+ CategoryForm,
52
+ ConsumptionForm,
53
+ IngredientForm,
54
+ MealTypeForm,
55
+ PurchasedIngredientsWorkBookForm,
56
+ )
57
+ from .models import Category, Consumption, Ingredient, MealType
58
+
59
+ # Create your views here.
60
+ decimal_prec = getattr(settings, "DECIMAL_PREC", 2)
61
+
62
+ storage_date_header = (
63
+ _("Storage Date"),
64
+ _(
65
+ 'Formats like "YYYY.mm.dd", "YYYY/mm/dd", '
66
+ + '"YYYY.mm.dd", "mm/dd", "mmdd", and "mm.dd" '
67
+ + "are all acceptable. In short, FNSCHOOL wants to be "
68
+ + "compatible with all the formats you like, but if "
69
+ + "something goes wrong, you have to tell the "
70
+ + "developers. Thank you!"
71
+ ),
72
+ )
73
+ ingredient_name_header = (_("Ingredient Name"), _("Name of Ingredient"))
74
+ meal_type_header = (
75
+ _("Meal Type"),
76
+ _(
77
+ "For example, breakfast, dinner, regular meals, "
78
+ + "nutritious meals, etc., when generating a spreadsheet,"
79
+ + " each meal category corresponds to a spread sheet. "
80
+ + "If left blank, only one spreadsheet will be generated."
81
+ ),
82
+ )
83
+
84
+ category_header = (
85
+ _("Category"),
86
+ _(
87
+ "Usually there are seven categories: vegetables, meat, "
88
+ + "grains, seasonings, eggs and milk, oils, and fruits."
89
+ ),
90
+ )
91
+ quantity_header = (
92
+ _("Quantity"),
93
+ _(
94
+ "To prevent the number of decimal places in the unit "
95
+ + 'price from becoming too large, "quantity" is '
96
+ + "only allowed to be an integer."
97
+ ),
98
+ )
99
+
100
+
101
+ total_price_header = (
102
+ _("Total Price"),
103
+ None,
104
+ )
105
+ quantity_unit_name_header = (
106
+ _("Unit Name of Quantity"),
107
+ None,
108
+ )
109
+ is_ignorable_header = (
110
+ _("Is Ignorable"),
111
+ _("As long as a cell has content, it will be considered " + 'as "yes".'),
112
+ )
113
+
114
+
115
+ date_patterns = [
116
+ (r"\b\d{4}-\d{2}-\d{2}\b", "%Y-%m-%d"),
117
+ (r"\b\d{4}/\d{2}/\d{2}\b", "%Y/%m/%d"),
118
+ (r"\b\d{4}\.\d{2}\.\d{2}\b", "%Y.%m.%d"),
119
+ (r"\b\d{8}\b", "%Y%m%d"),
120
+ ]
121
+
122
+
123
+ def get_decimal_places(decimal_number):
124
+ if not isinstance(decimal_number, Decimal):
125
+ decimal_number = Decimal(str(decimal_number))
126
+ tuple_rep = decimal_number.as_tuple()
127
+ return abs(tuple_rep.exponent) if tuple_rep.exponent < 0 else 0
128
+
129
+
130
+ def split_price(total_price, quantity, prec=2):
131
+ prec = prec
132
+ prec_decimal = Decimal("0." + ("0" * prec))
133
+
134
+ total_price0 = Decimal(str(total_price))
135
+ quantity0 = Decimal(str(quantity))
136
+ unit_price0 = total_price0 / quantity0
137
+
138
+ unit_price_floor = unit_price0.quantize(prec_decimal, rounding=ROUND_FLOOR)
139
+
140
+ if unit_price_floor == unit_price0:
141
+ return [[total_price0, quantity0], [None, None]]
142
+
143
+ total_price_floor = quantity0 * unit_price_floor
144
+ total_price_diff = total_price0 - total_price_floor
145
+
146
+ split_quantity = total_price_diff * Decimal(str(10**prec))
147
+
148
+ unit_price0 = unit_price_floor
149
+ quantity0 = quantity0 - split_quantity
150
+ total_price0 = quantity0 * unit_price0
151
+
152
+ unit_price1 = unit_price_floor + Decimal(str(1 / (10**prec)))
153
+ quantity1 = split_quantity
154
+ total_price1 = unit_price1 * quantity1
155
+
156
+ return [[total_price0, quantity0], [total_price1, quantity1]]
157
+
158
+
159
+ def get_consumption_ingredients(request):
160
+ date_start = request.GET.get(
161
+ "storage_date_start", None
162
+ ) or request.COOKIES.get("storage_date_start", None)
163
+ date_end = request.GET.get("storage_date_end", None) or request.COOKIES.get(
164
+ "storage_date_end", None
165
+ )
166
+ ingredients = Ingredient.objects
167
+ queries = Q()
168
+ queries = (
169
+ Q(user=request.user) & Q(is_disabled=False) & Q(is_ignorable=False)
170
+ )
171
+ if date_start:
172
+ date_start = date_parser.parse(date_start).date()
173
+ queries &= Q(storage_date__gte=date_start)
174
+ if date_end:
175
+ date_end = date_parser.parse(date_end).date()
176
+ queries &= Q(storage_date__lte=date_end)
177
+
178
+ if not date_start and not date_end:
179
+ queries &= Q(quantity__gt=F("total_consumed"))
180
+ ingredients = ingredients.annotate(
181
+ total_consumed=Coalesce(
182
+ Sum("consumptions__amount_used"), 0, output_field=IntegerField()
183
+ )
184
+ )
185
+
186
+ ingredients = ingredients.filter(queries).order_by(
187
+ "name", "storage_date", "meal_type__name", "category__name"
188
+ )
189
+
190
+ return ingredients
191
+
192
+
193
+ def get_date_range(ingredients):
194
+ date_start = ingredients.order_by("storage_date").first().storage_date
195
+ today = datetime.now().date()
196
+ date_end = (today.replace(day=1) + relativedelta(months=2)) - relativedelta(
197
+ days=1
198
+ )
199
+ date_range = list(pd.date_range(start=date_start, end=date_end))
200
+ date_range = [d.date() for d in date_range]
201
+
202
+ return date_range
203
+
204
+
205
+ @login_required
206
+ @require_POST
207
+ def new_consumption(request, consumption_id=None):
208
+ form = None
209
+ if consumption_id:
210
+ consumption = Consumption.objects.filter(
211
+ Q(pk=consumption_id)
212
+ & Q(ingredient__user=request.user)
213
+ & Q(is_disabled=False)
214
+ ).first()
215
+ if consumption:
216
+
217
+ posted_amount_used = request.POST.get("amount_used")
218
+ if (
219
+ posted_amount_used.replace(".", "").isnumeric()
220
+ and Decimal(posted_amount_used).is_zero()
221
+ ):
222
+ consumption.delete()
223
+ return HttpResponse("OK", status=201)
224
+
225
+ form = ConsumptionForm(request.POST, instance=consumption)
226
+ else:
227
+ return HttpResponse("Accepted", status=202)
228
+
229
+ else:
230
+ ingredient = Ingredient.objects.filter(
231
+ Q(user=request.user) & Q(pk=request.POST.get("ingredient"))
232
+ ).first()
233
+ if not ingredient:
234
+ return HttpResponse("Accepted", status=202)
235
+
236
+ consumption = Consumption()
237
+ consumption.ingredient = ingredient
238
+ form = ConsumptionForm(request.POST, instance=consumption)
239
+
240
+ if form.is_valid() and not form.instance.is_disabled:
241
+ consumption = form.save(commit=False)
242
+ consumption.save()
243
+ return HttpResponse("OK", status=201)
244
+
245
+ return HttpResponse("Accepted", status=202)
246
+
247
+
248
+ @login_required
249
+ def create_consumptions(request, ingredient_id=None):
250
+ ingredients = get_consumption_ingredients(request)
251
+ date_range = None
252
+ if not ingredients:
253
+ return render(
254
+ request,
255
+ "canteen/consumption/create.html",
256
+ {"ingredients": ingredients, "date_range": date_range},
257
+ )
258
+
259
+ date_range = get_date_range(ingredients)
260
+ date_end = request.GET.get("storage_date_end", None) or request.COOKIES.get(
261
+ "storage_date_end", None
262
+ )
263
+ if date_end:
264
+ date_end = date_parser.parse(date_end).date()
265
+ date_range = [d for d in date_range if d <= date_end]
266
+
267
+ if ingredient_id:
268
+ ingredient = get_object_or_404(Ingredient, pk=ingredient_id)
269
+ planned_consumptions = []
270
+ consumptions = ingredient.consumptions.filter(
271
+ Q(is_disabled=False)
272
+ ).all()
273
+ consumption_dates = list(set([c.date_of_using for c in consumptions]))
274
+
275
+ for per_day in date_range:
276
+ consumption = None
277
+ if per_day in consumption_dates:
278
+ consumptions_per_day = [
279
+ c for c in consumptions if c.date_of_using == per_day
280
+ ]
281
+ consumption = consumptions_per_day[0]
282
+ if len(consumptions_per_day) > 1:
283
+ for c in consumptions_per_day[1:]:
284
+ c.delete()
285
+
286
+ else:
287
+ consumption = Consumption()
288
+ consumption.ingredient = ingredient
289
+ consumption.date_of_using = per_day
290
+
291
+ if per_day < ingredient.storage_date:
292
+ consumption.is_disabled = True
293
+
294
+ planned_consumptions.append(consumption)
295
+
296
+ form_list = []
297
+ for c in planned_consumptions:
298
+ form = ConsumptionForm(instance=c)
299
+ form.fields["date_of_using"].label = ""
300
+ form_list.append(form)
301
+
302
+ return render(
303
+ request,
304
+ "canteen/consumption/_create.html",
305
+ {"form_list": form_list},
306
+ )
307
+
308
+ ingredients = ingredients.all()
309
+ ingredients_pinned = []
310
+ ingredients_unpinned = []
311
+ categories_top = Category.objects.filter(
312
+ Q(user=request.user)
313
+ & Q(pin_to_consumptions_top=True)
314
+ & Q(is_disabled=False)
315
+ ).all()
316
+
317
+ for i in ingredients:
318
+ if i.category in categories_top:
319
+ ingredients_pinned.append(i)
320
+ else:
321
+ ingredients_unpinned.append(i)
322
+
323
+ ingredients = ingredients_pinned + ingredients_unpinned
324
+
325
+ date_range_cp = date_range
326
+ date_range_cp = [d.strftime("%Y-%m-%d") for d in date_range]
327
+ meal_types = list(set([i.meal_type.name for i in ingredients]))
328
+ ingredient_ids = [i.id for i in ingredients]
329
+ months = list(set([d.strftime("%Y-%m") for d in date_range]))
330
+ months = sorted(months, key=lambda d: int(d.split("-")[1]))
331
+ return render(
332
+ request,
333
+ "canteen/consumption/create.html",
334
+ {
335
+ "ingredients": ingredients,
336
+ "date_range": date_range_cp,
337
+ "meal_types": meal_types,
338
+ "ingredient_ids": ingredient_ids,
339
+ "months": months,
340
+ },
341
+ )
342
+
343
+
344
+ @login_required()
345
+ def delete_ingredient(request, ingredient_id):
346
+ ingredient = get_object_or_404(Ingredient, pk=ingredient_id)
347
+
348
+ if request.method == "POST":
349
+ if ingredient.user == request.user:
350
+ ingredient.delete()
351
+ return render(
352
+ request,
353
+ "canteen/close.html",
354
+ )
355
+
356
+ form = IngredientForm(instance=ingredient)
357
+ return render(request, "canteen/ingredient/delete.html", {"form": form})
358
+
359
+
360
+ def edit_ingredient(request, ingredient_id):
361
+ ingredient = get_object_or_404(Ingredient, pk=ingredient_id)
362
+
363
+ if request.method == "POST":
364
+ form = IngredientForm(request.POST, instance=ingredient)
365
+ if form.is_valid():
366
+ form.save()
367
+ return render(
368
+ request,
369
+ "canteen/close.html",
370
+ )
371
+ else:
372
+ form = IngredientForm(instance=ingredient)
373
+
374
+ return render(request, "canteen/ingredient/update.html", {"form": form})
375
+
376
+
377
+ @login_required
378
+ def list_ingredients(request):
379
+ search_query = request.GET.get("q", "")
380
+ search_query_cp = search_query
381
+ fields = [
382
+ f for f in Ingredient._meta.fields if f.name not in ["id", "user"]
383
+ ]
384
+
385
+ if search_query:
386
+ queries = Q(user=request.user)
387
+
388
+ search_query_dates = []
389
+
390
+ for pattern, fmt in date_patterns:
391
+ matches = re.findall(pattern, search_query)
392
+ for match in matches:
393
+ try:
394
+ date_obj = datetime.strptime(match, fmt).date()
395
+ search_query_dates.append(date_obj)
396
+ search_query = search_query.replace(match, "")
397
+
398
+ except ValueError:
399
+ continue
400
+
401
+ if len(search_query_dates) > 1:
402
+ queries &= Q(storage_date__gte=min(search_query_dates))
403
+ queries &= Q(storage_date__lte=max(search_query_dates))
404
+ elif len(search_query_dates) == 1:
405
+ queries &= Q(storage_date=search_query_dates[0])
406
+
407
+ unit_names = Ingredient.objects.values("quantity_unit_name").distinct()
408
+ unit_names = [
409
+ c.get("quantity_unit_name")
410
+ for c in unit_names
411
+ if c.get("quantity_unit_name") in str(search_query)
412
+ ]
413
+ for unit_name in unit_names:
414
+ queries &= Q(quantity_unit_name__icontains=unit_name)
415
+ search_query = search_query.replace(unit_name, "")
416
+
417
+ categories = Ingredient.objects.values("category__name").distinct()
418
+ categories = [
419
+ c.get("category__name")
420
+ for c in categories
421
+ if c.get("category__name") in search_query
422
+ ]
423
+ for category in categories:
424
+ queries &= Q(category__icontains=category)
425
+ search_query = search_query.replace(category, "")
426
+
427
+ meal_types = Ingredient.objects.values("meal_type__name").distinct()
428
+ meal_types = [
429
+ m.get("meal_type__name")
430
+ for m in meal_types
431
+ if m.get("meal_type__name") in search_query
432
+ ]
433
+ for meal_type in meal_types:
434
+ queries &= Q(meal_type__icontains=meal_type)
435
+ search_query = search_query.replace(meal_type, "")
436
+
437
+ names = re.split(r"\s+", search_query)
438
+ name_queries = Q()
439
+ for name in names:
440
+ name_queries |= Q(name__icontains=name)
441
+ queries &= name_queries
442
+
443
+ ingredients = Ingredient.objects.filter(queries)
444
+
445
+ else:
446
+ ingredients = Ingredient.objects.filter(Q(user=request.user))
447
+
448
+ orders = []
449
+ for f in fields:
450
+ sort_name = request.GET.get("sort_" + f.name, "")
451
+ if sort_name and sort_name in "+-":
452
+ sort_name = (
453
+ sort_name[1:] if sort_name.startswith("+") else sort_name
454
+ )
455
+ sort_name += f.name
456
+ orders.append(sort_name)
457
+ if len(orders) < 1:
458
+ ingredients = ingredients.order_by("storage_date", "category")
459
+ else:
460
+ ingredients = ingredients.order_by(*orders)
461
+
462
+ page_size = request.GET.get("page_size", "")
463
+ if not page_size:
464
+ page_size = request.COOKIES.get("page_size", "")
465
+ page_size = int(page_size) if str(page_size).isnumeric() else 10
466
+ paginator = Paginator(ingredients, page_size)
467
+ page_number = request.GET.get("page", 1)
468
+ page_obj = paginator.get_page(page_number)
469
+ headers = [
470
+ (f.name, request.GET.get("sort_" + f.name, ""), f.verbose_name)
471
+ for f in fields
472
+ ]
473
+ context = {
474
+ "page_obj": page_obj,
475
+ "search_query": search_query_cp,
476
+ "headers": headers,
477
+ "page_size": page_size,
478
+ }
479
+ return render(request, "canteen/ingredient/list.html", context)
480
+
481
+
482
+ def create_ingredients(request):
483
+ if request.method == "POST":
484
+ form = PurchasedIngredientsWorkBookForm(request.POST, request.FILES)
485
+ if form.is_valid():
486
+ workbook_file = request.FILES["workbook_file"]
487
+
488
+ if not workbook_file.name.endswith(".xlsx"):
489
+ return HttpResponse(_('Please upload a file in "xlsx" format.'))
490
+
491
+ df = pd.read_excel(workbook_file)
492
+
493
+ for index, row in df.iterrows():
494
+ category_name = row[category_header[0]]
495
+ meal_type_name = row[meal_type_header[0]]
496
+
497
+ category = Category.objects.filter(
498
+ Q(name=category_name) & Q(user=request.user)
499
+ ).first()
500
+ if not category:
501
+ category = Category.objects.create(
502
+ user=request.user,
503
+ name=category_name,
504
+ created_at=datetime.now().date(),
505
+ )
506
+
507
+ meal_type = MealType.objects.filter(
508
+ Q(name=meal_type_name) & Q(user=request.user)
509
+ ).first()
510
+ if not meal_type:
511
+ meal_type = MealType.objects.create(
512
+ user=request.user,
513
+ name=meal_type_name,
514
+ created_at=datetime.now().date(),
515
+ )
516
+
517
+ storage_date = row[storage_date_header[0]]
518
+ name = row[ingredient_name_header[0]]
519
+ quantity_unit_name = row[quantity_unit_name_header[0]]
520
+ is_ignorable = not row[is_ignorable_header[0]] is np.nan
521
+
522
+ total_price0 = row[total_price_header[0]]
523
+ quantity0 = row[quantity_header[0]]
524
+
525
+ [total_price0, quantity0], [total_price1, quantity1] = (
526
+ split_price(total_price0, quantity0)
527
+ )
528
+
529
+ if total_price1:
530
+
531
+ Ingredient.objects.create(
532
+ user=request.user,
533
+ storage_date=storage_date,
534
+ name=name + _("(1)"),
535
+ meal_type=meal_type,
536
+ category=category,
537
+ quantity=quantity0,
538
+ total_price=total_price0,
539
+ quantity_unit_name=quantity_unit_name,
540
+ is_ignorable=is_ignorable,
541
+ )
542
+
543
+ Ingredient.objects.create(
544
+ user=request.user,
545
+ storage_date=storage_date,
546
+ name=name + _("(2)"),
547
+ meal_type=meal_type,
548
+ category=category,
549
+ quantity=quantity1,
550
+ total_price=total_price1,
551
+ quantity_unit_name=quantity_unit_name,
552
+ is_ignorable=is_ignorable,
553
+ )
554
+
555
+ else:
556
+ Ingredient.objects.create(
557
+ user=request.user,
558
+ storage_date=storage_date,
559
+ name=name,
560
+ meal_type=meal_type,
561
+ category=category,
562
+ quantity=quantity0,
563
+ total_price=total_price0,
564
+ quantity_unit_name=quantity_unit_name,
565
+ is_ignorable=is_ignorable,
566
+ )
567
+
568
+ return redirect("canteen:close_window")
569
+
570
+ else:
571
+ form = PurchasedIngredientsWorkBookForm()
572
+ return render(request, "canteen/ingredient/create.html", {"form": form})
573
+
574
+
575
+ def get_template_workbook_of_purchased_ingredients(request):
576
+ global storage_date_header, ingredient_name_header, meal_type_header
577
+ global quantity_header, quantity_unit_name_header, total_price_header
578
+ global is_ignorable_header
579
+ headers = [
580
+ storage_date_header,
581
+ ingredient_name_header,
582
+ meal_type_header,
583
+ category_header,
584
+ quantity_header,
585
+ quantity_unit_name_header,
586
+ total_price_header,
587
+ is_ignorable_header,
588
+ ]
589
+
590
+ wb = Workbook()
591
+ ws = wb.active
592
+ ws.title = _("Purchased Ingredients Sheet")
593
+
594
+ for i, (h, c) in enumerate(headers):
595
+ h_cell = ws.cell(1, i + 1)
596
+
597
+ center_alignment = Alignment(horizontal="center", vertical="center")
598
+ h_cell.alignment = center_alignment
599
+
600
+ mono_font = Font(
601
+ name="Mono",
602
+ size=12,
603
+ )
604
+ h_cell.font = mono_font
605
+
606
+ h_cell.value = h
607
+ column_letter = get_column_letter(i + 1)
608
+ hans_len = count_chinese_characters(h)
609
+ hans_len = (hans_len + 2) if hans_len else hans_len
610
+ ws.column_dimensions[column_letter].width = len(h) + hans_len + 2
611
+ if c:
612
+ h_cell.comment = Comment(c, _("the FNSCHOOL Authors"))
613
+
614
+ buffer = io.BytesIO()
615
+ wb.save(buffer)
616
+ buffer.seek(0)
617
+
618
+ response = HttpResponse(
619
+ buffer,
620
+ content_type=(
621
+ "application/"
622
+ + "vnd.openxmlformats-officedocument.spreadsheetml.sheet"
623
+ ),
624
+ )
625
+ today = datetime.now().date()
626
+ last_month = today + relativedelta(months=-1, day=1)
627
+ filename = (
628
+ _("Purchased Ingredients WorkBook ({0})").format(
629
+ last_month.strftime("%Y%m")
630
+ )
631
+ + ".xlsx"
632
+ )
633
+
634
+ encoded_filename = escape_uri_path(filename)
635
+ response["Content-Disposition"] = (
636
+ f'attachment; filename="{encoded_filename}"'
637
+ )
638
+
639
+ return response
640
+
641
+
642
+ def close_window(request):
643
+ return render(request, "canteen/close.html")
644
+
645
+
646
+ def generate_spreadsheet(request, month):
647
+ from .workbook.generate import get_workbook_zip
648
+
649
+ buffer = get_workbook_zip(request, month)
650
+ filename = (
651
+ _("Canteen Daybook WorkBook ({month}) of {affiliation}").format(
652
+ month=month.replace("-", ""), affiliation=request.user.affiliation
653
+ )
654
+ + ".zip"
655
+ )
656
+ filename = escape_uri_path(filename)
657
+ response = HttpResponse(buffer, content_type="application/zip")
658
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
659
+
660
+ return response
661
+
662
+
663
+ class MealTypeDeleteView(LoginRequiredMixin, DeleteView):
664
+ model = MealType
665
+ template_name = "canteen/meal_type/delete.html"
666
+ success_url = reverse_lazy("canteen:close_window")
667
+ context_object_name = "meal_type"
668
+
669
+ def get_object(self, queryset=None):
670
+ meal_type = super().get_object(queryset)
671
+ if meal_type.user != self.request.user:
672
+ raise Http404()
673
+ return meal_type
674
+
675
+
676
+ class MealTypeUpdateView(LoginRequiredMixin, UpdateView):
677
+ model = MealType
678
+ template_name = "canteen/meal_type/update.html"
679
+ success_url = reverse_lazy("canteen:close_window")
680
+ form_class = MealTypeForm
681
+
682
+ def get_object(self, queryset=None):
683
+ meal_type = super().get_object(queryset)
684
+ if meal_type.user != self.request.user:
685
+ raise Http404()
686
+ return meal_type
687
+
688
+ def get_initial(self):
689
+ return {"user": self.request.user, "created_at": datetime.now().date}
690
+
691
+
692
+ class MealTypeCreateView(LoginRequiredMixin, CreateView):
693
+ model = MealType
694
+ template_name = "canteen/meal_type/create.html"
695
+ success_url = reverse_lazy("canteen:close_window")
696
+ form_class = MealTypeForm
697
+
698
+ def get_initial(self):
699
+ return {"user": self.request.user, "created_at": datetime.now().date}
700
+
701
+ def form_valid(self, form):
702
+ meal_type_saved = MealType.objects.filter(
703
+ Q(name=form.instance.name) & Q(user=self.request.user)
704
+ ).first()
705
+ if meal_type_saved:
706
+ return redirect("canteen:close_window")
707
+ form.instance.user = self.request.user
708
+ form.instance.created_at = datetime.now().date()
709
+ return super().form_valid(form)
710
+
711
+
712
+ class MealTypeListView(LoginRequiredMixin, ListView):
713
+ model = MealType
714
+ template_name = "canteen/meal_type/list.html"
715
+ context_object_name = "meal_types"
716
+ ordering = ["-created_at"]
717
+
718
+ paginate_by = 10
719
+ paginate_orphans = 2
720
+
721
+ def get_paginate_by(self, queryset):
722
+ page_size = self.request.GET.get("page_size")
723
+ page_size = (
724
+ page_size if page_size else self.request.COOKIES.get("page_size")
725
+ )
726
+ page_size = page_size if page_size else self.paginate_by
727
+ return int(page_size)
728
+
729
+
730
+ class CategoryDeleteView(LoginRequiredMixin, DeleteView):
731
+ model = Category
732
+ template_name = "canteen/category/delete.html"
733
+ success_url = reverse_lazy("canteen:close_window")
734
+ context_object_name = "category"
735
+
736
+ def get_object(self, queryset=None):
737
+ category = super().get_object(queryset)
738
+ if category.user != self.request.user:
739
+ raise Http404()
740
+ return category
741
+
742
+
743
+ class CategoryUpdateView(LoginRequiredMixin, UpdateView):
744
+ model = Category
745
+ template_name = "canteen/category/update.html"
746
+ success_url = reverse_lazy("canteen:close_window")
747
+ form_class = CategoryForm
748
+
749
+ def get_object(self, queryset=None):
750
+ category = super().get_object(queryset)
751
+ if category.user != self.request.user:
752
+ raise Http404()
753
+ return category
754
+
755
+ def get_initial(self):
756
+ return {"user": self.request.user, "created_at": datetime.now().date}
757
+
758
+
759
+ class CategoryCreateView(LoginRequiredMixin, CreateView):
760
+ model = Category
761
+ template_name = "canteen/category/create.html"
762
+ success_url = reverse_lazy("canteen:close_window")
763
+ form_class = CategoryForm
764
+
765
+ def get_initial(self):
766
+ return {"user": self.request.user, "created_at": datetime.now().date}
767
+
768
+ def form_valid(self, form):
769
+ category_saved = Category.objects.filter(
770
+ Q(name=form.instance.name) & Q(user=self.request.user)
771
+ ).first()
772
+ if category_saved:
773
+ return redirect("canteen:close_window")
774
+ form.instance.user = self.request.user
775
+ form.instance.created_at = datetime.now().date()
776
+ return super().form_valid(form)
777
+
778
+
779
+ class CategoryListView(LoginRequiredMixin, ListView):
780
+ model = Category
781
+ template_name = "canteen/category/list.html"
782
+ context_object_name = "categories"
783
+ ordering = ["-created_at"]
784
+
785
+ paginate_by = 10
786
+ paginate_orphans = 2
787
+
788
+ def get_paginate_by(self, queryset):
789
+ page_size = self.request.GET.get("page_size")
790
+ page_size = (
791
+ page_size if page_size else self.request.COOKIES.get("page_size")
792
+ )
793
+ page_size = page_size if page_size else self.paginate_by
794
+ return int(page_size)
795
+
796
+
797
+ class IngredientCreateView(LoginRequiredMixin, CreateView):
798
+ model = Ingredient
799
+ template_name = "canteen/ingredient/create_one.html"
800
+ success_url = reverse_lazy("canteen:close_window")
801
+ form_class = IngredientForm
802
+
803
+ def get_initial(self):
804
+ return {
805
+ "user": self.request.user,
806
+ "storage_date": datetime.now().date(),
807
+ }
808
+
809
+ def form_valid(self, form):
810
+ form.instance.user = self.request.user
811
+ return super().form_valid(form)
812
+
813
+
814
+ # The end.