Feature/event notifications (#399)
* additional server events * sort 'recent recipes' by updated * remove duplicate code * fixes #396 * set color * consolidate tag/category pages * set colors * list unorganized recipes * cleanup old code * remove flash message, switch to global snackbar * cancel to close * cleanup * notifications first pass * test notification * complete notification feature * use background tasks * add url param * update documentation Co-authored-by: hay-kot <hay-kot@pm.me>
Before Width: | Height: | Size: 58 KiB |
BIN
docs/docs/assets/img/add-notification.webp
Normal file
After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 214 KiB |
BIN
docs/docs/assets/img/add-user.webp
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 46 KiB |
BIN
docs/docs/assets/img/admin-backup.webp
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 34 KiB |
@ -1,233 +0,0 @@
|
|||||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="672px" height="802px" viewBox="-0.5 -0.5 672 802" content="<mxfile host="23d2e92c-1798-4628-b714-afc635cb8bb4" modified="2021-02-25T18:35:08.654Z" agent="5.0 (Macintosh; Intel Mac OS X 11_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Code-Insiders/1.54.0-insider Chrome/87.0.4280.141 Electron/11.3.0 Safari/537.36" etag="VkjSc3HAwyf7iAB2Toef" version="14.2.4" type="embed"><diagram id="3j9sfWdaUOHFNAte1u_k" name="Page-1">7VvbcuM2Ev0aVW0e7KJ4keRH3zK7VU6tKzOpPKYgEiKxJgkuCVlyvj7duJAgCVnyhJqZeMYPtgg0cenuc7rRkGfBbbH/UJMq+4UnNJ/5XrKfBXcz35/74RL+YMuLbvGuQtWS1izRbV3DR/YnNYK6dcsS2vQEBee5YFW/MeZlSWPRayN1zXd9sQ3P+7NWJKWjho8xycetv7NEZKp1FXld+78pSzMz89zTPWsSP6U135Z6vpkf/Cx/VHdBzFhavslIwndWU3A/C25rzoX6VOxvaY7KNWpT7/18oLddd01LccoLoX7jmeRbapYsFyZejDJwP5UWo7Wge5cJyNqIe+M1zNudgctQXlBRv4DIvm904yzBSj3vOs2HRiaztH6l24g2dtqO3G0YPug9u/cfeMf3X3FWClrfP8N+cKr5LLhp7ebBQ0KajCb6IRNFboREzZ/oLc95DS0lL2HAm5ysaf7IGyYYL6E5pjg4dKBuGbjgw0BgzYXghSVwnbMUOwSvoJXop3YcWFqFKy/2KaLzsmhiQi9hC9uSXm4bWv+hDBrcbFiem9WBo95Hq8CPoH1swNZNhvYfG9Wy2tJhtMUURls6jLbIBWp/3rPd4v9bRNLNhpfiopE0cw0C81W17zrhU4p/fwPdQOcH1E5jBoS14JhKwjRWg1dS/Qqs3OPYYM1MCpa/2JO10sBUFH9LEOU5gAgNDiNs4BcaCmVERkQn2jQ8ZkSAsyEhplRktL6Ez79pafAcXqasTGW/PdeOSRVm5FkOFMe0aYwQjMLq4fAFJXmVk1IutEyG3UiBDU79KWMoIn/F2xp9BveLg+JeSvmwoURs5Rbk5tSq4O0FKdARy3VTWeoFnVedygdoBP8TJ8DM9m3d1FQkRtUEd1H39AlBdHfhI3R3GRP0I7TjVDsAD7RxcPlNLqGesSShALUbye8t4A8y4xhABynQIMUCTxCeifJC/9uj/PH2nYw/9yfYv+9ij/NQPpBqsrxae94ZeJ9vRc5KmMpkQN4pwYBvNixWYaC5JEnBSgZLJgKX+5Z44B9wjq8TDxZniQfXqJ4TA8EvwJZMMqt+STN2s61khBjQuZOGidzUVoWUhAgMDP+i+zjfJorRK2DgHa+T5ick3kcgJl4XZk5lRybHFaR5wmFz9qTWpAfQq8AwVdOGCtE128MrCUxldX/V8jkua00aqkQKluKcUkgt+LaNFgUpiY5E4NJSF2pCGTWubdeToYOUUgUNt/UCqxQ1i1XIqWnMKjoKigWpn2S/5mcdgiHbSZjiKNhLF5Fws99V5PEniTyTUG8YfP3Q4/dDjx86Yo93rtPGCaetb/u0MUHUkWxzLNgcc+rgaLBZnCvYXJ3t8NG8QkuvxJ5rSbFlmht6Q5zUVCfqkvokuUrOtWOQPDGQhsWo3Zo955jTm2iE7DnrzgZVzcFimvfV6C3Va1GbnynOynclcv39uNui75ILm7v1UaPP7IbJXbz93fD4AZff9137i5wgouM0ZiggfgG+SGgdHNCOZYW1UsrDum1oi2n/VbRjzKXqhfPo79LImyJF4FCxi2RWU2j4PBntXZu9eb/SnCDlNxmrPpN3Psl8yhBIbQ3Y4hgYAGhNtcgKQcIIhIRCMRMFVCBB/AeB32S8Fl2OqilADTDTdYOOQtTwg0qKLjn0XsEyxoWsY+BMN2jehG7INpcM1GbcVolkrVNmXWRpx+3U2Q4wLOrIzeCspF2s2Xg7YAaCaaZldALN26y1340AkMn3LqOaLDuKp3vIn5uBku09QL7d30G3cRkKLt8xW76O7FV0FNnuJDiYANvmEsHC9q/aVYYcumMFeC7qMs5YnjyQF8i/oK8R4Bjm6QaAw/4E+JPOQKQW+lIl8HoSH/FNrchOrXNNIPqd+co867V0Vje3IT6+T5FvHo3WvbbpgTTiaOY6ShodHjSBqeeLPosvVmNbz100PsmBJxon/BcdRUmmzgHFs0jnfxEyOoSxOWrowsp8lOya85xiLteTMTjpSQxcyWg7pxvxdgg/yLfuwoGbLA4eE6Qf6EOacjVBhPUM9jMecpKNI89tZAPgE206RfIThSOTqluAH+CdHLxhH7uR77Czq6g4DXbHWe7FTBfbXoOjvl2zsC1vlMbYNtmMLaojQU/23YE5fDOYXUaeBMzjuwCs5D7mDgr9Aei/D+jI/6rReOVEtAXYD13x8R3hbfnNBM+FP7KAW+U/wDZB9PSPh0/fVeqeBGyLca3/oj1sn5D52t8/0JItNUd37w2jiwO3NIcx6rTcJBgd157QRJf6YkaVM7Aygeq0Ssyz7qszHl//D68HpqsljCAzNEsBhyCcxllU6BcQbAv700CtLfoZqEUOUnUZLJzAYMsTbtX6GjhSiB0qyGmkKQgq7CcD5h6/pzQHPU3h5K5bnIHOaJJSEwYamhawq/uu6YaWyTV+1RS9MicNVkEPatFEFVelevSVkUGPHXfUGnFhb1W5pdPIoVPTpqq4z/3hXYrWMzzibaVl0au+ReeDERq+rWOqX+qsNRpnPhho6Q1sDmE+pWI0EBiEvFhi8jK1OXm9ZprOidSAnUu1Gj0NmSdclHyTyPS9foUUPPKLQXN5wiX5xNB8TxCcr8KB6Vafh8Fw8CW9aBirJsJgW1EbLPjQukbyofdZ+5gM5K67un8CyEN/GH/DLwfyE753/wPkh+OW3w9c0dVnBtpoAI6zBVr/1Ug7XpfnXtdkoB0X/uQhp7thPnDMMVd1+ojTjLz23Z5xBtn60vGl7fMdccaVO3nWvDRVgsMG6+q536HJhug+o83gsfu3NYXI7p8Dg/u/AA==</diagram></mxfile>" style="background-color: rgb(255, 255, 255);">
|
|
||||||
<defs/>
|
|
||||||
<g>
|
|
||||||
<rect x="0" y="138" width="70" height="60" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<path d="M 0 190.12 C 0.33 185.06 0.82 180.02 1.48 175 C 1.8 173.47 2.19 171.97 2.66 170.5 C 3.13 168.98 4 167.67 5.14 166.72 C 6.56 165.61 8.15 164.81 9.83 164.35 C 10.51 164.04 11.26 164.04 11.94 164.35 C 16.84 169.6 24.48 169.6 29.38 164.35 C 29.98 164.08 30.64 164.08 31.24 164.35 C 33.19 165.09 35.06 166.07 36.82 167.27 C 34.65 167.54 32.55 168.25 30.62 169.39 C 27.64 171.19 25.41 174.27 24.43 177.95 L 22.92 190.14 Z M 20.17 161.73 C 17.61 161.8 15.14 160.56 13.36 158.3 C 11.58 156.05 10.65 152.98 10.79 149.85 C 10.72 146.76 11.68 143.77 13.45 141.58 C 15.23 139.39 17.66 138.19 20.17 138.26 C 22.78 138 25.35 139.12 27.25 141.33 C 29.14 143.54 30.18 146.64 30.09 149.85 C 30.25 153.1 29.25 156.28 27.34 158.56 C 25.44 160.84 22.83 162 20.17 161.73 Z M 25.84 198 C 26.15 193.52 26.59 189.05 27.17 184.6 C 27.42 182.59 27.8 180.6 28.32 178.66 C 28.64 176.59 29.65 174.75 31.15 173.52 C 32.85 172.3 34.66 171.28 36.55 170.5 C 37.4 170.21 38.32 170.4 39.03 171 C 40.08 172.2 41.31 173.19 42.66 173.92 C 46.02 175.58 49.83 175.58 53.19 173.92 C 54.53 173.16 55.78 172.22 56.9 171.1 C 57.79 170.31 59.01 170.19 60 170.8 C 61.6 171.42 63.13 172.3 64.51 173.42 C 65.8 174.41 66.77 175.86 67.26 177.55 C 68.13 180.7 68.72 183.94 69.03 187.22 C 69.43 190.81 69.76 194.4 70 198 Z M 48.5 167.98 C 42.29 168.13 37.15 161.83 36.99 153.87 C 37.03 150.02 38.26 146.34 40.42 143.65 C 42.58 140.97 45.49 139.5 48.5 139.57 C 54.38 140.09 58.97 146.32 59.02 153.87 C 58.89 161.35 54.33 167.47 48.5 167.98 Z" fill="#e58325" stroke="none" pointer-events="all"/>
|
|
||||||
<rect x="70" y="138" width="340" height="90" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 332px; height: 1px; padding-top: 128px; margin-left: 75px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: left; max-height: 100px; overflow: hidden; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
|
||||||
<h1 style="font-size: 18px">
|
|
||||||
User Groups
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
User groups, or "family" groups are a collection of users that are associated together. Users belonging to groups will have access to their associated mealplans and associated pages. This is currently the only feature of groups.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="75" y="140" fill="#000000" font-family="Helvetica" font-size="12px">
|
|
||||||
User Groups...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<rect x="0" y="10" width="70" height="60" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<path d="M 63.22 68.4 C 63.82 68.4 64.58 67.93 64.58 67.14 C 64.58 66.64 64.03 65.99 63.22 65.99 C 62.34 65.99 61.86 66.7 61.86 67.14 C 61.86 67.83 62.49 68.4 63.22 68.4 Z M 66.39 67.22 C 66.39 68.69 64.91 70 63.27 70 C 61.44 70 60.07 68.65 60.07 67.25 L 60.07 50.06 C 58.43 49.33 56.45 47.65 56.45 44.65 C 56.55 42.25 58.13 40.43 60.07 39.51 L 60.07 44.39 L 63.22 45.99 L 66.39 44.39 L 66.39 39.5 C 68.29 40.34 70 42.32 70 44.78 C 69.95 47.1 68.53 49.03 66.39 50.06 Z M 49.19 69.99 C 48.73 69.99 48.32 69.65 48.32 69.19 L 48.32 57.17 C 48.32 56.81 48.7 56.39 49.17 56.39 L 50.12 56.39 L 50.13 45.21 L 49.77 42.55 L 49.77 38.79 L 52.29 38.79 L 52.3 42.55 L 51.94 45.21 L 51.94 56.39 L 52.85 56.39 C 53.37 56.39 53.74 56.68 53.74 57.21 L 53.74 69.22 C 53.74 69.68 53.33 69.99 52.88 69.99 Z M 27.78 36.63 C 20.5 36.63 14 31.25 14 22.98 C 14 16.57 19.5 10 27.52 10 C 35.36 10 41.1 16.36 41.1 23.29 C 41.1 31.31 34.33 36.63 27.78 36.63 Z M 0 63.51 C 0.63 57.61 1.47 49.54 3.07 45.46 C 3.81 43.61 4.88 42.28 6.61 41.17 C 7.73 40.43 11.72 38.48 12.45 38.16 C 13.73 37.57 15.05 37.26 16.18 38.16 C 22.94 43.46 32.3 43.3 39.15 38.18 C 39.94 37.46 41.21 37.65 42.04 37.94 C 42.97 38.26 45.37 39.46 46.96 40.29 L 46.96 42.69 L 47.32 45.36 L 47.32 54.39 C 46.31 54.92 45.52 55.93 45.52 57.17 L 45.51 63.51 Z" fill="#e58325" stroke="#d79b00" stroke-miterlimit="10" pointer-events="all"/>
|
|
||||||
<rect x="70" y="10" width="340" height="120" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 332px; height: 1px; padding-top: 0px; margin-left: 75px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: left; max-height: 130px; overflow: hidden; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
|
||||||
<h1 style="font-size: 18px">
|
|
||||||
Admins
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
Mealie admins are super users that have access to all user data (excluding passwords). Perform administrative tasks like adding users, resetting user passwords, backing up the database, migrating data, and managing site settings. Administrators can also access restricted recipes that are marked hidden or uneditable by the user.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="75" y="12" fill="#000000" font-family="Helvetica" font-size="12px">
|
|
||||||
Admins...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<rect x="10" y="240" width="60" height="60" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<path d="M 40.01 269.86 C 32.14 269.86 25.13 263.82 25.13 254.55 C 25.13 247.37 31.06 240 39.72 240 C 48.19 240 54.39 247.13 54.39 254.9 C 54.39 263.89 47.08 269.86 40.01 269.86 Z M 10 300 C 10.68 293.37 11.59 284.33 13.32 279.75 C 14.11 277.68 15.27 276.19 17.14 274.95 C 18.35 274.12 22.66 271.93 23.45 271.57 C 24.83 270.91 26.26 270.57 27.48 271.57 C 34.78 277.52 44.88 277.34 52.28 271.6 C 53.14 270.78 54.51 271 55.41 271.32 C 56.82 271.82 61.36 274.29 62.22 274.79 C 64.73 276.3 66.08 278.32 67.09 282.11 C 68.37 287.23 69.13 293.75 70 300 Z" fill="#e58325" stroke="none" pointer-events="all"/>
|
|
||||||
<rect x="70" y="240" width="340" height="90" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 332px; height: 1px; padding-top: 230px; margin-left: 75px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: left; max-height: 100px; overflow: hidden; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
|
||||||
<h1 style="font-size: 18px">
|
|
||||||
Users
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
A single user created by an Admin that has basic privlages to edit their profile, create and edit recipes they own. Edit recipes that are not hidden and are marked editable.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="75" y="242" fill="#000000" font-family="Helvetica" font-size="12px">
|
|
||||||
Users...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<path d="M 10 375 C 10 366.72 23.43 360 40 360 C 47.96 360 55.59 361.58 61.21 364.39 C 66.84 367.21 70 371.02 70 375 L 70 425 C 70 433.28 56.57 440 40 440 C 23.43 440 10 433.28 10 425 Z" fill="#e58325" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
|
||||||
<path d="M 70 375 C 70 383.28 56.57 390 40 390 C 23.43 390 10 383.28 10 375" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
|
||||||
<rect x="75" y="360" width="340" height="130" fill="none" stroke="none" pointer-events="all"/>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 332px; height: 1px; padding-top: 350px; margin-left: 80px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: left; max-height: 140px; overflow: hidden; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
|
||||||
<h1 style="font-size: 18px">
|
|
||||||
Database Relationships
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
The basic relationship and ownership is diagramed below. In short users are owners of recipes and groups are the owners of meal-plans. By default all users will be added to the "default" group. If a recipe is added through a migration or through a backup where no user exists ownership will be set to the default Admin.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="80" y="362" fill="#000000" font-family="Helvetica" font-size="12px">
|
|
||||||
Database Relationships...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<path d="M 310 710 L 310 693.5 Q 310 680 296.5 680 L 163.5 680 Q 150 680 150 693.5 L 150 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
|
||||||
<path d="M 150 710 L 150 786.5 Q 150 800 163.5 800 L 296.5 800 Q 310 800 310 786.5 L 310 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 150 710 L 310 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<g fill="#000000" font-family="Helvetica" font-weight="bold" pointer-events="none" text-anchor="middle" font-size="18px">
|
|
||||||
<text x="229.5" y="702.5">
|
|
||||||
Recipe
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<g fill="#000000" font-family="Helvetica" pointer-events="none" font-size="16px">
|
|
||||||
<text x="155.5" y="731.5">
|
|
||||||
- owners: list[Users]
|
|
||||||
</text>
|
|
||||||
<text x="155.5" y="750.5">
|
|
||||||
- editable: boolean
|
|
||||||
</text>
|
|
||||||
<text x="155.5" y="769.5">
|
|
||||||
- hidden: boolean
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<path d="M 200 550 L 200 533.5 Q 200 520 186.5 520 L 43.5 520 Q 30 520 30 533.5 L 30 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 30 550 L 30 626.5 Q 30 640 43.5 640 L 186.5 640 Q 200 640 200 626.5 L 200 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 30 550 L 200 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<g fill="#000000" font-family="Helvetica" font-weight="bold" pointer-events="none" text-anchor="middle" font-size="18px">
|
|
||||||
<text x="114.5" y="542.5">
|
|
||||||
User
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<g fill="#000000" font-family="Helvetica" pointer-events="none" font-size="16px">
|
|
||||||
<text x="35.5" y="571.5">
|
|
||||||
- admin: boolean
|
|
||||||
</text>
|
|
||||||
<text x="35.5" y="590.5">
|
|
||||||
- group: list[Group]
|
|
||||||
</text>
|
|
||||||
<text x="35.5" y="609.5">
|
|
||||||
- recipes: list[Recipe]
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<path d="M 670 710 L 670 693.5 Q 670 680 656.5 680 L 523.5 680 Q 510 680 510 693.5 L 510 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 510 710 L 510 786.5 Q 510 800 523.5 800 L 656.5 800 Q 670 800 670 786.5 L 670 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 510 710 L 670 710" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<g fill="#000000" font-family="Helvetica" font-weight="bold" pointer-events="none" text-anchor="middle" font-size="18px">
|
|
||||||
<text x="589.5" y="702.5">
|
|
||||||
MealPlan
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<g fill="#000000" font-family="Helvetica" pointer-events="none" font-size="16px">
|
|
||||||
<text x="515.5" y="731.5">
|
|
||||||
- group: Group
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<path d="M 610 550 L 610 533.5 Q 610 520 596.5 520 L 423.5 520 Q 410 520 410 533.5 L 410 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 410 550 L 410 626.5 Q 410 640 423.5 640 L 596.5 640 Q 610 640 610 626.5 L 610 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 410 550 L 610 550" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<g fill="#000000" font-family="Helvetica" font-weight="bold" pointer-events="none" text-anchor="middle" font-size="18px">
|
|
||||||
<text x="509.5" y="542.5">
|
|
||||||
Group
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<g fill="#000000" font-family="Helvetica" pointer-events="none" font-size="16px">
|
|
||||||
<text x="415.5" y="571.5">
|
|
||||||
- users: list[Users]
|
|
||||||
</text>
|
|
||||||
<text x="415.5" y="590.5">
|
|
||||||
- mealplans list[MealPlan]
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 570px; margin-left: 271px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: none; white-space: normal; word-wrap: normal; ">
|
|
||||||
User.group is backfilled by a Groups object
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="320" y="574" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
|
||||||
User.group is bac...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<rect x="34" y="636" width="10" height="10" fill="#ffffff" stroke="none" pointer-events="none"/>
|
|
||||||
<path d="M 39 611 L 39 690 Q 39 700 49 700 L 130.76 700" fill="none" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 136.76 700 L 128.76 704 L 130.76 700 L 128.76 696 Z" fill="#e58325" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<rect x="195" y="583" width="10" height="10" fill="#ffffff" stroke="none" pointer-events="none"/>
|
|
||||||
<path d="M 174 588 L 234 588 Q 244 588 244 578 L 244 550 Q 244 540 254 540 L 391.76 540" fill="none" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 397.76 540 L 389.76 544 L 391.76 540 L 389.76 536 Z" fill="#e58325" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<rect x="414" y="634" width="10" height="10" fill="#ffffff" stroke="none" pointer-events="none"/>
|
|
||||||
<path d="M 419 591 L 419 690 Q 419 700 429 700 L 480 700 Q 490 700 490.88 700 L 491.76 700" fill="none" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<path d="M 497.76 700 L 489.76 704 L 491.76 700 L 489.76 696 Z" fill="#e58325" stroke="#e58325" stroke-width="2" stroke-miterlimit="10" pointer-events="none"/>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 730px; margin-left: 35px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: none; white-space: normal; word-wrap: normal; ">
|
|
||||||
User.recipes is backfilled by Recipe objects
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="84" y="734" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
|
||||||
User.recipes is b...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(-0.5 -0.5)">
|
|
||||||
<switch>
|
|
||||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 730px; margin-left: 401px;">
|
|
||||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
|
||||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: none; white-space: normal; word-wrap: normal; ">
|
|
||||||
Group.mealplan is backfilled by MealPlan objects
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</foreignObject>
|
|
||||||
<text x="450" y="734" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
|
||||||
Group.mealplan is...
|
|
||||||
</text>
|
|
||||||
</switch>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<switch>
|
|
||||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
|
||||||
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
|
||||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
|
||||||
Viewer does not support full SVG 1.1
|
|
||||||
</text>
|
|
||||||
</a>
|
|
||||||
</switch>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 138 KiB |
BIN
docs/docs/assets/img/mealie-diagram.webp
Normal file
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 482 KiB |
BIN
docs/docs/assets/img/sign-up-links.webp
Normal file
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 175 KiB |
BIN
docs/docs/assets/img/site-settings.webp
Normal file
After Width: | Height: | Size: 21 KiB |
59
docs/docs/getting-started/notifications.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Getting External Notifications
|
||||||
|
|
||||||
|
## Apprise
|
||||||
|
|
||||||
|
Using the [Apprise](https://github.com/caronc/apprise/) library Mealie is able to provided notification services for nearly every popular service. Some of our favorites are...
|
||||||
|
|
||||||
|
- [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify)
|
||||||
|
- [Discord](https://github.com/caronc/apprise/wiki/Notify_discord)
|
||||||
|
- [Home Assistant](https://github.com/caronc/apprise/wiki/Notify_homeassistant)
|
||||||
|
- [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix)
|
||||||
|
- [Pushover](https://github.com/caronc/apprise/wiki/Notify_pushover)
|
||||||
|
|
||||||
|
But there are some many to choose from! Take a look at their wiki for information on how to create their URL formats and that you can use to create a notification integration in Mealie.
|
||||||
|
|
||||||
|
|
||||||
|
## Subscribe Events
|
||||||
|
There are several categories of events that mealie logs that can be broadcast with the notifications feature. You can also see a feed of your events in the Admin Dashboard
|
||||||
|
|
||||||
|
- General Events
|
||||||
|
- Application Startup
|
||||||
|
- Recipe Events
|
||||||
|
- Create Recipe
|
||||||
|
- Delete Recipe
|
||||||
|
- Database Events
|
||||||
|
- Export/Import
|
||||||
|
- Database Initialization
|
||||||
|
- Scheduled Events
|
||||||
|
- MealPlan Webhooks Sent
|
||||||
|
- Group Events
|
||||||
|
- Create/Delete Groups
|
||||||
|
- User Events
|
||||||
|
- User Creation
|
||||||
|
- User Sign-up
|
||||||
|
- Sign-up Token Creation
|
||||||
|
- Invalid login attempts
|
||||||
|
|
||||||
|
In most cases the events will also provide details on which user performed the action. Now you'll know when your grandma deletes your favorite recipe!
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
This is a new feature and we are still working through all the possibilities of events. if you have an idea for an event let us know!
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a New Notification
|
||||||
|
|
||||||
|
New events can be created and viewed in admin Toolbox `/admin/toolbox?tab=event-notifications`. Select the "+ Notification" button and you'll be provided with a dialog. Complete the form using the URL for the service you'd like to connect to. Before saving be sure to use the test feature.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
The feedback provided from the test feature is only an indicated of if the URL you provided is valid, not if the message was successfully sent. Be sure to check the notification feed for the test message.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
![Discord]()
|
||||||
|
|
||||||
|
### Gotify
|
||||||
|
![Gotify]()
|
@ -19,4 +19,4 @@ Below are some general guidelines that were considered when creating the organiz
|
|||||||
|
|
||||||
In the diagram below you will see what we came up with using the new custom pages feature. The large circles indicate pages, and the rectangles indicate categories. We've grouped several 'like' categories with each other as a way to quickly find similar items.
|
In the diagram below you will see what we came up with using the new custom pages feature. The large circles indicate pages, and the rectangles indicate categories. We've grouped several 'like' categories with each other as a way to quickly find similar items.
|
||||||
|
|
||||||

|

|
@ -10,7 +10,7 @@ Your sites settings panel can only be accessed by administrators. This where you
|
|||||||
| Card Per Section | The amount of cards displayed in each section on the home page |
|
| Card Per Section | The amount of cards displayed in each section on the home page |
|
||||||
| Home Page Sections | Category sections to include on the home page |
|
| Home Page Sections | Category sections to include on the home page |
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ There are two ways to create users in Mealie.
|
|||||||
### Manually Creating a User
|
### Manually Creating a User
|
||||||
In the Manage Users section you are able to create a user by providing the necessary information in the pop-up dialog.
|
In the Manage Users section you are able to create a user by providing the necessary information in the pop-up dialog.
|
||||||
|
|
||||||
{: align=right style="height:50%;width:50%"}
|
{: align=right style="height:50%;width:50%"}
|
||||||
|
|
||||||
- User Name
|
- User Name
|
||||||
- Email
|
- Email
|
||||||
@ -69,7 +69,7 @@ When creating users manually, their password will be set from the default assign
|
|||||||
### Sign Up Links
|
### Sign Up Links
|
||||||
You can generate sign-up links in the Manage Users section. Select the "create link" button and provide the name of the link and if the user will be an administrator. Once a link is created it will populate in the table where you'll be able to see all active links, delete a link, and copy the link as needed.
|
You can generate sign-up links in the Manage Users section. Select the "create link" button and provide the name of the link and if the user will be an administrator. Once a link is created it will populate in the table where you'll be able to see all active links, delete a link, and copy the link as needed.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
When a link is used it is automatically removed from the database.
|
When a link is used it is automatically removed from the database.
|
||||||
|
@ -55,6 +55,7 @@ nav:
|
|||||||
- Working With Recipes: "getting-started/recipes.md"
|
- Working With Recipes: "getting-started/recipes.md"
|
||||||
- Organizing Recipes: "getting-started/organizing-recipes.md"
|
- Organizing Recipes: "getting-started/organizing-recipes.md"
|
||||||
- Planning Meals: "getting-started/meal-planner.md"
|
- Planning Meals: "getting-started/meal-planner.md"
|
||||||
|
- External Notifications: "getting-started/notifications.md"
|
||||||
- iOS Shortcuts: "getting-started/ios.md"
|
- iOS Shortcuts: "getting-started/ios.md"
|
||||||
- Site Administration:
|
- Site Administration:
|
||||||
- User Settings: "site-administration/user-settings.md"
|
- User Settings: "site-administration/user-settings.md"
|
||||||
|
1
frontend/public/static/discord.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#7289DA;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/static/gotify.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
frontend/public/static/home-assistant.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/static/matrix.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
7
frontend/public/static/pushover.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="602px" height="602px" viewBox="57 57 602 602" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="layer1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(58.964119, 58.887520)" opacity="0.91">
|
||||||
|
<ellipse style="fill: rgb(36, 157, 241); fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0;" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)" cx="216.308" cy="152.076" rx="296.855" ry="296.855"/>
|
||||||
|
<path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); fill-rule: nonzero; white-space: pre;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
@ -1,5 +1,6 @@
|
|||||||
import { baseURL } from "./api-utils";
|
import { baseURL } from "./api-utils";
|
||||||
import { apiReq } from "./api-utils";
|
import { apiReq } from "./api-utils";
|
||||||
|
import i18n from "@/i18n.js";
|
||||||
|
|
||||||
const prefix = baseURL + "about";
|
const prefix = baseURL + "about";
|
||||||
|
|
||||||
@ -12,6 +13,10 @@ const aboutURLs = {
|
|||||||
statistics: `${prefix}/statistics`,
|
statistics: `${prefix}/statistics`,
|
||||||
events: `${prefix}/events`,
|
events: `${prefix}/events`,
|
||||||
event: id => `${prefix}/events/${id}`,
|
event: id => `${prefix}/events/${id}`,
|
||||||
|
|
||||||
|
allNotifications: `${prefix}/events/notifications`,
|
||||||
|
testNotifications: `${prefix}/events/notifications/test`,
|
||||||
|
notification: id => `${prefix}/events/notifications/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const aboutAPI = {
|
export const aboutAPI = {
|
||||||
@ -27,6 +32,39 @@ export const aboutAPI = {
|
|||||||
const resposne = await apiReq.delete(aboutURLs.events);
|
const resposne = await apiReq.delete(aboutURLs.events);
|
||||||
return resposne.data;
|
return resposne.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async allEventNotifications() {
|
||||||
|
const response = await apiReq.get(aboutURLs.allNotifications);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createNotification(data) {
|
||||||
|
const response = await apiReq.post(aboutURLs.allNotifications, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteNotification(id) {
|
||||||
|
const response = await apiReq.delete(aboutURLs.notification(id));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
async testNotificationByID(id) {
|
||||||
|
const response = await apiReq.post(
|
||||||
|
aboutURLs.testNotifications,
|
||||||
|
{ id: id },
|
||||||
|
() => i18n.t("events.something-went-wrong"),
|
||||||
|
() => i18n.t("events.test-message-sent")
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
async testNotificationByURL(url) {
|
||||||
|
const response = await apiReq.post(
|
||||||
|
aboutURLs.testNotifications,
|
||||||
|
{ test_url: url },
|
||||||
|
() => i18n.t("events.something-went-wrong"),
|
||||||
|
() => i18n.t("events.test-message-sent")
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
// async getAppInfo() {
|
// async getAppInfo() {
|
||||||
// const response = await apiReq.get(aboutURLs.version);
|
// const response = await apiReq.get(aboutURLs.version);
|
||||||
// return response.data;
|
// return response.data;
|
||||||
|
@ -71,7 +71,7 @@ export const userAPI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
delete(id) {
|
delete(id) {
|
||||||
return apiReq.delete(usersURLs.userID(id), null, deleteErrorText, function() {
|
return apiReq.delete(usersURLs.userID(id), null, deleteErrorText, () => {
|
||||||
return i18n.t("user.user-deleted");
|
return i18n.t("user.user-deleted");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<slot name="open" v-bind="{ open }"> </slot>
|
<slot name="open" v-bind="{ open }"> </slot>
|
||||||
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
|
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
|
||||||
<v-card class="pb-10" height="100%">
|
<v-card height="100%">
|
||||||
<v-app-bar dark :color="color" class="mt-n1 mb-0">
|
<v-app-bar dark :color="color" class="mt-n1 mb-0">
|
||||||
<v-icon large left>
|
<v-icon large left>
|
||||||
{{ titleIcon }}
|
{{ titleIcon }}
|
||||||
@ -11,7 +11,9 @@
|
|||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
|
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||||
<slot> </slot>
|
|
||||||
|
<slot v-bind="{ submitEvent }"> </slot>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<slot name="card-actions">
|
<slot name="card-actions">
|
||||||
<v-btn text color="grey" @click="dialog = false">
|
<v-btn text color="grey" @click="dialog = false">
|
||||||
@ -22,13 +24,15 @@
|
|||||||
<v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete">
|
<v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete">
|
||||||
{{ $t("general.delete") }}
|
{{ $t("general.delete") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn color="success" @click="submitEvent">
|
<v-btn color="success" type="submit" @click="submitEvent">
|
||||||
{{ submitText }}
|
{{ submitText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</slot>
|
</slot>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<slot name="below-actions"> </slot>
|
<div class="pb-4" v-if="$slots['below-actions']">
|
||||||
|
<slot name="below-actions"> </slot>
|
||||||
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -59,6 +63,9 @@ export default {
|
|||||||
submitText: {
|
submitText: {
|
||||||
default: () => i18n.t("general.create"),
|
default: () => i18n.t("general.create"),
|
||||||
},
|
},
|
||||||
|
keepOpen: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -68,7 +75,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
determineClose() {
|
determineClose() {
|
||||||
return this.submitted && !this.loading;
|
return this.submitted && !this.loading && !this.keepOpen;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -82,6 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submitEvent() {
|
submitEvent() {
|
||||||
|
console.log("Submit");
|
||||||
this.$emit("submit");
|
this.$emit("submit");
|
||||||
this.submitted = true;
|
this.submitted = true;
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-center ma-2">
|
<div class="text-center ma-2">
|
||||||
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
|
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
|
||||||
|
<v-icon dark left>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
|
||||||
{{ snackbar.title }}
|
{{ snackbar.title }}
|
||||||
{{ snackbar.text }}
|
{{ snackbar.text }}
|
||||||
|
|
||||||
@ -25,6 +29,18 @@ export default {
|
|||||||
return this.$store.getters.getSnackbar;
|
return this.$store.getters.getSnackbar;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
icon() {
|
||||||
|
switch (this.snackbar.color) {
|
||||||
|
case "error":
|
||||||
|
return "mdi-alert";
|
||||||
|
case "success":
|
||||||
|
return "mdi-checkbox-marked-circle";
|
||||||
|
case "info":
|
||||||
|
return "mdi-information";
|
||||||
|
default:
|
||||||
|
return "mdi-bell-alert";
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
@ -31,6 +31,16 @@
|
|||||||
"category-updated": "Category updated",
|
"category-updated": "Category updated",
|
||||||
"uncategorized-count": "Uncategorized {count}"
|
"uncategorized-count": "Uncategorized {count}"
|
||||||
},
|
},
|
||||||
|
"events": {
|
||||||
|
"notification": "Notification",
|
||||||
|
"apprise-url": "Apprise URL",
|
||||||
|
"subscribed-events": "Subscribed Events",
|
||||||
|
"scheduled": "Scheduled",
|
||||||
|
"database": "Database",
|
||||||
|
"test-message-sent": "Test Message Sent",
|
||||||
|
"something-went-wrong": "Something Went Wrong!",
|
||||||
|
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features."
|
||||||
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@ -55,6 +65,7 @@
|
|||||||
"file-uploaded": "File uploaded",
|
"file-uploaded": "File uploaded",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"friday": "Friday",
|
"friday": "Friday",
|
||||||
|
"general": "General",
|
||||||
"get": "Get",
|
"get": "Get",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"image-upload-failed": "Image upload failed",
|
"image-upload-failed": "Image upload failed",
|
||||||
@ -70,6 +81,7 @@
|
|||||||
"random": "Random",
|
"random": "Random",
|
||||||
"rating": "Rating",
|
"rating": "Rating",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
|
"recipe": "Recipe",
|
||||||
"recipes": "Recipes",
|
"recipes": "Recipes",
|
||||||
"rename-object": "Rename {0}",
|
"rename-object": "Rename {0}",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
@ -88,6 +100,8 @@
|
|||||||
"thursday": "Thursday",
|
"thursday": "Thursday",
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"tuesday": "Tuesday",
|
"tuesday": "Tuesday",
|
||||||
|
"type": "Type",
|
||||||
|
"test": "Test",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated": "Updated",
|
"updated": "Updated",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<base-dialog
|
<BaseDialog
|
||||||
ref="assignDialog"
|
ref="assignDialog"
|
||||||
title-icon="mdi-tag"
|
title-icon="mdi-tag"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -33,7 +33,7 @@
|
|||||||
:single-column="true"
|
:single-column="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</base-dialog>
|
</BaseDialog>
|
||||||
|
|
||||||
<v-btn @click="openDialog" small color="success">
|
<v-btn @click="openDialog" small color="success">
|
||||||
{{ $t("settings.toolbox.bulk-assign") }}
|
{{ $t("settings.toolbox.bulk-assign") }}
|
||||||
|
236
frontend/src/pages/Admin/ToolBox/EventNotification.vue
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-card outlined class="mt-n1">
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseDialog
|
||||||
|
:keep-open="keepDialogOpen"
|
||||||
|
title-icon="mdi-bell-alert"
|
||||||
|
:title="$t('general.new') + ' ' + $t('events.notification')"
|
||||||
|
@submit="createNotification"
|
||||||
|
>
|
||||||
|
<template v-slot:open="{ open }">
|
||||||
|
<v-btn small color="info" @click="open">
|
||||||
|
<v-icon left>
|
||||||
|
mdi-plus
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("events.notification") }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:default>
|
||||||
|
<v-card-text class="mt-2">
|
||||||
|
{{ $t("events.new-notification-form-description") }}
|
||||||
|
|
||||||
|
<div class="d-flex justify-space-around mt-1 mb-3">
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki" target="_blanks"> Apprise </a>
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki/Notify_gotify" target="_blanks"> Gotify </a>
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki/Notify_discord" target="_blanks"> Discord </a>
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki/Notify_homeassistant" target="_blanks">
|
||||||
|
Home Assistant
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki/Notify_matrix" target="_blanks"> Matrix </a>
|
||||||
|
<a href="https://github.com/caronc/apprise/wiki/Notify_pushover" target="_blanks"> Pushover </a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-form ref="notificationForm">
|
||||||
|
<v-select
|
||||||
|
:label="$t('general.type')"
|
||||||
|
:rules="[existsRule]"
|
||||||
|
:items="notificationTypes"
|
||||||
|
item-value="text"
|
||||||
|
v-model="newNotification.type"
|
||||||
|
>
|
||||||
|
</v-select>
|
||||||
|
<v-text-field :rules="[existsRule]" :label="$t('general.name')" v-model="newNotification.name">
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
required
|
||||||
|
:rules="[existsRule]"
|
||||||
|
:label="$t('events.apprise-url')"
|
||||||
|
v-model="newNotification.notificationUrl"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn class="d-flex ml-auto" small color="info" @click="testByURL(newNotification.notificationUrl)">
|
||||||
|
<v-icon left> mdi-test-tube</v-icon>
|
||||||
|
{{ $t("general.test") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-subheader class="pa-0 mb-0">
|
||||||
|
{{ $t("events.subscribed-events") }}
|
||||||
|
</v-subheader>
|
||||||
|
<v-row class="mt-1">
|
||||||
|
<v-col cols="3" v-for="(item, key, index) in newNotificationOptions" :key="index">
|
||||||
|
<v-checkbox class="my-n3 py-0" v-model="newNotificationOptions[key]" :label="key"> </v-checkbox>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
</v-card-actions>
|
||||||
|
<v-simple-table>
|
||||||
|
<template v-slot:default>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("general.type") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("general.name") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("general.general") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("general.recipe") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("events.database") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("events.scheduled") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("settings.migrations") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("group.group") }}
|
||||||
|
</th>
|
||||||
|
<th class="text-center">
|
||||||
|
{{ $t("user.user") }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, index) in notifications" :key="index">
|
||||||
|
<td>
|
||||||
|
<v-avatar size="35" class="ma-1" :color="getIcon(item.type).icon ? 'primary' : undefined">
|
||||||
|
<v-icon dark v-if="getIcon(item.type).icon"> {{ getIcon(item.type).icon }}</v-icon>
|
||||||
|
<v-img v-else :src="getIcon(item.type).image"> </v-img>
|
||||||
|
</v-avatar>
|
||||||
|
{{ item.type }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.name }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.general ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.recipe ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.backup ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.scheduled ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.migration ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.group ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<v-icon color="success"> {{ item.user ? "mdi-check" : "" }} </v-icon>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<v-btn class="mx-1" small color="error" @click="deleteNotification(item.id)">
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
{{ $t("general.delete") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn small color="info" @click="testByID(item.id)">
|
||||||
|
<v-icon left> mdi-test-tube</v-icon>
|
||||||
|
{{ $t("general.test") }}
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
|
import { api } from "@/api";
|
||||||
|
import { validators } from "@/mixins/validators";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BaseDialog,
|
||||||
|
},
|
||||||
|
mixins: [validators],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keepDialogOpen: false,
|
||||||
|
notifications: [],
|
||||||
|
newNotification: {
|
||||||
|
type: "General",
|
||||||
|
name: "",
|
||||||
|
notificationUrl: "",
|
||||||
|
},
|
||||||
|
newNotificationOptions: {
|
||||||
|
general: true,
|
||||||
|
recipe: true,
|
||||||
|
backup: true,
|
||||||
|
scheduled: true,
|
||||||
|
migration: true,
|
||||||
|
group: true,
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
notificationTypes: [
|
||||||
|
{
|
||||||
|
text: "General",
|
||||||
|
icon: "mdi-bell-alert",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Discord",
|
||||||
|
image: "./static/discord.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Gotify",
|
||||||
|
image: "./static/gotify.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Home Assistant",
|
||||||
|
image: "./static/home-assistant.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Pushover",
|
||||||
|
image: "./static/pushover.svg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getAllNotifications();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getIcon(textValue) {
|
||||||
|
return this.notificationTypes.find(x => x.text === textValue);
|
||||||
|
},
|
||||||
|
async getAllNotifications() {
|
||||||
|
this.notifications = await api.about.allEventNotifications();
|
||||||
|
},
|
||||||
|
async createNotification() {
|
||||||
|
if (this.$refs.notificationForm.validate()) {
|
||||||
|
this.keepDialogOpen = false;
|
||||||
|
await api.about.createNotification({ ...this.newNotification, ...this.newNotificationOptions });
|
||||||
|
this.getAllNotifications();
|
||||||
|
} else {
|
||||||
|
this.keepDialogOpen = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteNotification(id) {
|
||||||
|
await api.about.deleteNotification(id);
|
||||||
|
this.getAllNotifications();
|
||||||
|
},
|
||||||
|
async testByID(id) {
|
||||||
|
await api.about.testNotificationByID(id);
|
||||||
|
},
|
||||||
|
async testByURL(url) {
|
||||||
|
await api.about.testNotificationByURL(url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -4,6 +4,10 @@
|
|||||||
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
|
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
|
||||||
<v-tabs-slider></v-tabs-slider>
|
<v-tabs-slider></v-tabs-slider>
|
||||||
|
|
||||||
|
<v-tab href="#event-notifications">
|
||||||
|
Notify
|
||||||
|
<v-icon>mdi-bell-alert</v-icon>
|
||||||
|
</v-tab>
|
||||||
<v-tab href="#category-editor">
|
<v-tab href="#category-editor">
|
||||||
{{ $t("recipe.categories") }}
|
{{ $t("recipe.categories") }}
|
||||||
<v-icon>mdi-tag-multiple-outline</v-icon>
|
<v-icon>mdi-tag-multiple-outline</v-icon>
|
||||||
@ -20,20 +24,23 @@
|
|||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-tabs-items v-model="tab">
|
<v-tabs-items v-model="tab">
|
||||||
|
<v-tab-item value="event-notifications"> <EventNotification /></v-tab-item>
|
||||||
<v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item>
|
<v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item>
|
||||||
<v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item>
|
<v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item>
|
||||||
<v-tab-item value="organize"> <RecipeOrganizer :is-tags="true" /> </v-tab-item>
|
<v-tab-item value="organize"> <RecipeOrganizer /> </v-tab-item>
|
||||||
</v-tabs-items>
|
</v-tabs-items>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import EventNotification from "./EventNotification";
|
||||||
import CategoryTagEditor from "./CategoryTagEditor";
|
import CategoryTagEditor from "./CategoryTagEditor";
|
||||||
import RecipeOrganizer from "./RecipeOrganizer";
|
import RecipeOrganizer from "./RecipeOrganizer";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CategoryTagEditor,
|
CategoryTagEditor,
|
||||||
|
EventNotification,
|
||||||
RecipeOrganizer,
|
RecipeOrganizer,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -95,6 +95,7 @@ def determine_sqlite_path(path=False, suffix=DB_VERSION) -> str:
|
|||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
global DATA_DIR
|
global DATA_DIR
|
||||||
PRODUCTION: bool = Field(True, env="PRODUCTION")
|
PRODUCTION: bool = Field(True, env="PRODUCTION")
|
||||||
|
BASE_URL: str = "http://localhost:8080"
|
||||||
IS_DEMO: bool = False
|
IS_DEMO: bool = False
|
||||||
API_PORT: int = 9000
|
API_PORT: int = 9000
|
||||||
API_DOCS: bool = True
|
API_DOCS: bool = True
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from mealie.db.db_base import BaseDocument
|
from mealie.db.db_base import BaseDocument
|
||||||
from mealie.db.models.event import Event
|
from mealie.db.models.event import Event, EventNotification
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.mealplan import MealPlanModel
|
from mealie.db.models.mealplan import MealPlanModel
|
||||||
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
|
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
|
||||||
@ -10,6 +10,7 @@ from mealie.db.models.sign_up import SignUp
|
|||||||
from mealie.db.models.theme import SiteThemeModel
|
from mealie.db.models.theme import SiteThemeModel
|
||||||
from mealie.db.models.users import LongLiveToken, User
|
from mealie.db.models.users import LongLiveToken, User
|
||||||
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
|
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
|
||||||
|
from mealie.schema.event_notifications import EventNotificationIn
|
||||||
from mealie.schema.events import Event as EventSchema
|
from mealie.schema.events import Event as EventSchema
|
||||||
from mealie.schema.meal import MealPlanInDB
|
from mealie.schema.meal import MealPlanInDB
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
@ -156,6 +157,13 @@ class _Events(BaseDocument):
|
|||||||
self.schema = EventSchema
|
self.schema = EventSchema
|
||||||
|
|
||||||
|
|
||||||
|
class _EventNotification(BaseDocument):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.primary_key = "id"
|
||||||
|
self.sql_model = EventNotification
|
||||||
|
self.schema = EventNotificationIn
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.recipes = _Recipes()
|
self.recipes = _Recipes()
|
||||||
@ -170,6 +178,7 @@ class Database:
|
|||||||
self.groups = _Groups()
|
self.groups = _Groups()
|
||||||
self.custom_pages = _CustomPages()
|
self.custom_pages = _CustomPages()
|
||||||
self.events = _Events()
|
self.events = _Events()
|
||||||
|
self.event_notifications = _EventNotification()
|
||||||
|
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
@ -1,14 +1,45 @@
|
|||||||
import sqlalchemy as sa
|
|
||||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from sqlalchemy import Boolean, Column, DateTime, Integer, String
|
||||||
|
|
||||||
|
|
||||||
|
class EventNotification(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "event_notifications"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
type = Column(String)
|
||||||
|
notification_url = Column(String)
|
||||||
|
|
||||||
|
# Event Types
|
||||||
|
general = Column(Boolean, default=False)
|
||||||
|
recipe = Column(Boolean, default=False)
|
||||||
|
backup = Column(Boolean, default=False)
|
||||||
|
scheduled = Column(Boolean, default=False)
|
||||||
|
migration = Column(Boolean, default=False)
|
||||||
|
group = Column(Boolean, default=False)
|
||||||
|
user = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, *args, **kwargs
|
||||||
|
) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.notification_url = notification_url
|
||||||
|
self.type = type
|
||||||
|
self.general = general
|
||||||
|
self.recipe = recipe
|
||||||
|
self.backup = backup
|
||||||
|
self.scheduled = scheduled
|
||||||
|
self.migration = migration
|
||||||
|
self.group = group
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
|
||||||
class Event(SqlAlchemyBase, BaseMixins):
|
class Event(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "events"
|
__tablename__ = "events"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
title = sa.Column(sa.String)
|
title = Column(String)
|
||||||
text = sa.Column(sa.String)
|
text = Column(String)
|
||||||
time_stamp = sa.Column(sa.DateTime)
|
time_stamp = Column(DateTime)
|
||||||
category = sa.Column(sa.String)
|
category = Column(String)
|
||||||
|
|
||||||
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
|
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -1,28 +1,97 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from http.client import HTTPException
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.events import EventsOut
|
from mealie.schema.event_notifications import EventNotificationIn, EventNotificationOut
|
||||||
|
from mealie.schema.events import EventsOut, TestEvent
|
||||||
|
from mealie.schema.user import UserInDB
|
||||||
|
from mealie.services.events import test_notification
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
router = APIRouter(prefix="/events", tags=["App Events"])
|
router = APIRouter(prefix="/events", tags=["App Events"])
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=EventsOut)
|
@router.get("", response_model=EventsOut)
|
||||||
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
async def get_events(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
|
||||||
""" Get event from the Database """
|
""" Get event from the Database """
|
||||||
# Get Item
|
# Get Item
|
||||||
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
|
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("")
|
@router.delete("")
|
||||||
async def delete_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
async def delete_events(
|
||||||
|
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||||
|
):
|
||||||
""" Get event from the Database """
|
""" Get event from the Database """
|
||||||
# Get Item
|
# Get Item
|
||||||
return db.events.delete_all(session)
|
return db.events.delete_all(session)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_event(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
async def delete_event(
|
||||||
|
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||||
|
):
|
||||||
""" Delete event from the Database """
|
""" Delete event from the Database """
|
||||||
return db.events.delete(session, id)
|
return db.events.delete(session, id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/notifications")
|
||||||
|
async def create_event_notification(
|
||||||
|
event_data: EventNotificationIn,
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
""" Create event_notification in the Database """
|
||||||
|
|
||||||
|
return db.event_notifications.create(session, event_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/notifications/test")
|
||||||
|
async def test_notification_route(
|
||||||
|
test_data: TestEvent,
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
""" Create event_notification in the Database """
|
||||||
|
|
||||||
|
if test_data.id:
|
||||||
|
event_obj: EventNotificationIn = db.event_notifications.get(session, test_data.id)
|
||||||
|
test_data.test_url = event_obj.notification_url
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_notification(test_data.test_url)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/notifications", response_model=list[EventNotificationOut])
|
||||||
|
async def get_all_event_notification(
|
||||||
|
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
""" Get all event_notification from the Database """
|
||||||
|
# Get Item
|
||||||
|
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/notifications/{id}")
|
||||||
|
async def update_event_notification(
|
||||||
|
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
""" Update event_notification in the Database """
|
||||||
|
# Update Item
|
||||||
|
return {"details": "not yet implemented"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/notifications/{id}")
|
||||||
|
async def delete_event_notification(
|
||||||
|
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
""" Delete event_notification from the Database """
|
||||||
|
# Delete Item
|
||||||
|
return db.event_notifications.delete(session, id)
|
||||||
|
@ -2,7 +2,7 @@ import operator
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import app_dirs
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.core.security import create_file_token
|
from mealie.core.security import create_file_token
|
||||||
@ -33,7 +33,7 @@ def available_imports():
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/export/database", status_code=status.HTTP_201_CREATED)
|
@router.post("/export/database", status_code=status.HTTP_201_CREATED)
|
||||||
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
|
def export_database(background_tasks: BackgroundTasks, data: BackupJob, session: Session = Depends(generate_session)):
|
||||||
"""Generates a backup of the recipe database in json format."""
|
"""Generates a backup of the recipe database in json format."""
|
||||||
try:
|
try:
|
||||||
export_path = backup_all(
|
export_path = backup_all(
|
||||||
@ -47,7 +47,9 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
|
|||||||
export_users=data.options.users,
|
export_users=data.options.users,
|
||||||
export_groups=data.options.groups,
|
export_groups=data.options.groups,
|
||||||
)
|
)
|
||||||
create_backup_event("Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session)
|
background_tasks.add_task(
|
||||||
|
create_backup_event, "Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session
|
||||||
|
)
|
||||||
return {"export_path": export_path}
|
return {"export_path": export_path}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
@ -75,7 +77,12 @@ async def download_backup_file(file_name: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
|
@router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
|
||||||
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
|
def import_database(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
file_name: str,
|
||||||
|
import_data: ImportJob,
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
):
|
||||||
""" Import a database backup file generated from Mealie. """
|
""" Import a database backup file generated from Mealie. """
|
||||||
|
|
||||||
db_import = imports.import_database(
|
db_import = imports.import_database(
|
||||||
@ -90,7 +97,7 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
|
|||||||
force_import=import_data.force,
|
force_import=import_data.force,
|
||||||
rebase=import_data.rebase,
|
rebase=import_data.rebase,
|
||||||
)
|
)
|
||||||
create_backup_event("Database Restore", f"Restore File: {file_name}", session)
|
background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session)
|
||||||
return db_import
|
return db_import
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
@ -32,6 +32,7 @@ async def get_current_user_group(
|
|||||||
|
|
||||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||||
async def create_group(
|
async def create_group(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
group_data: GroupBase,
|
group_data: GroupBase,
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -40,7 +41,7 @@ async def create_group(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
db.groups.create(session, group_data.dict())
|
db.groups.create(session, group_data.dict())
|
||||||
create_group_event("Group Created", f"'{group_data.name}' created")
|
background_tasks.add_task(create_group_event, "Group Created", f"'{group_data.name}' created", session)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -58,7 +59,10 @@ async def update_group_data(
|
|||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_user_group(
|
async def delete_user_group(
|
||||||
id: int, current_user=Depends(get_current_user), session: Session = Depends(generate_session)
|
background_tasks: BackgroundTasks,
|
||||||
|
id: int,
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
""" Removes a user group from the database """
|
""" Removes a user group from the database """
|
||||||
|
|
||||||
@ -73,5 +77,8 @@ async def delete_user_group(
|
|||||||
if group.users != []:
|
if group.users != []:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
|
||||||
|
|
||||||
create_group_event("Group Deleted", f"'{group.name}' Deleted")
|
background_tasks.add_task(
|
||||||
|
create_group_event, "Group Deleted", f"'{group.name}' deleted by {current_user.full_name}", session
|
||||||
|
)
|
||||||
|
|
||||||
db.groups.delete(session, id)
|
db.groups.delete(session, id)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
@ -25,16 +25,22 @@ def get_all_meals(
|
|||||||
|
|
||||||
@router.post("/create", status_code=status.HTTP_201_CREATED)
|
@router.post("/create", status_code=status.HTTP_201_CREATED)
|
||||||
def create_meal_plan(
|
def create_meal_plan(
|
||||||
data: MealPlanIn, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
|
background_tasks: BackgroundTasks,
|
||||||
|
data: MealPlanIn,
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
""" Creates a meal plan database entry """
|
""" Creates a meal plan database entry """
|
||||||
processed_plan = process_meals(session, data)
|
processed_plan = process_meals(session, data)
|
||||||
create_group_event("Meal Plan Created", f"Mealplan Created for '{current_user.group}'")
|
background_tasks.add_task(
|
||||||
|
create_group_event, "Meal Plan Created", f"Mealplan Created for '{current_user.group}'", session=session
|
||||||
|
)
|
||||||
return db.meals.create(session, processed_plan.dict())
|
return db.meals.create(session, processed_plan.dict())
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{plan_id}")
|
@router.put("/{plan_id}")
|
||||||
def update_meal_plan(
|
def update_meal_plan(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
plan_id: str,
|
plan_id: str,
|
||||||
meal_plan: MealPlanIn,
|
meal_plan: MealPlanIn,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -45,13 +51,16 @@ def update_meal_plan(
|
|||||||
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
|
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
|
||||||
try:
|
try:
|
||||||
db.meals.update(session, plan_id, processed_plan.dict())
|
db.meals.update(session, plan_id, processed_plan.dict())
|
||||||
create_group_event("Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'")
|
background_tasks.add_task(
|
||||||
|
create_group_event, "Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'", session=session
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{plan_id}")
|
@router.delete("/{plan_id}")
|
||||||
def delete_meal_plan(
|
def delete_meal_plan(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
plan_id,
|
plan_id,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
@ -60,7 +69,9 @@ def delete_meal_plan(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
db.meals.delete(session, plan_id)
|
db.meals.delete(session, plan_id)
|
||||||
create_group_event("Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'")
|
background_tasks.add_task(
|
||||||
|
create_group_event, "Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'", session=session
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, status
|
||||||
from fastapi.datastructures import UploadFile
|
from fastapi.datastructures import UploadFile
|
||||||
|
from mealie.core.config import settings
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
||||||
|
from mealie.schema.user import UserInDB
|
||||||
from mealie.services.events import create_recipe_event
|
from mealie.services.events import create_recipe_event
|
||||||
from mealie.services.image.image import scrape_image, write_image
|
from mealie.services.image.image import scrape_image, write_image
|
||||||
from mealie.services.recipe.media import check_assets, delete_assets
|
from mealie.services.recipe.media import check_assets, delete_assets
|
||||||
@ -20,6 +22,7 @@ logger = get_logger()
|
|||||||
|
|
||||||
@router.post("/create", status_code=201, response_model=str)
|
@router.post("/create", status_code=201, response_model=str)
|
||||||
def create_from_json(
|
def create_from_json(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
data: Recipe,
|
data: Recipe,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
@ -27,22 +30,36 @@ def create_from_json(
|
|||||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||||
recipe: Recipe = db.recipes.create(session, data.dict())
|
recipe: Recipe = db.recipes.create(session, data.dict())
|
||||||
|
|
||||||
create_recipe_event("Recipe Created", f"Recipe '{recipe.name}' created", session=session)
|
background_tasks.add_task(
|
||||||
|
create_recipe_event,
|
||||||
|
"Recipe Created (URL)",
|
||||||
|
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
|
||||||
|
session=session,
|
||||||
|
attachment=recipe.image_dir.joinpath("min-original.webp"),
|
||||||
|
)
|
||||||
|
|
||||||
return recipe.slug
|
return recipe.slug
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create-url", status_code=201, response_model=str)
|
@router.post("/create-url", status_code=201, response_model=str)
|
||||||
def parse_recipe_url(
|
def parse_recipe_url(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
url: RecipeURLIn,
|
url: RecipeURLIn,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
current_user=Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||||
|
|
||||||
recipe = create_from_url(url.url)
|
recipe = create_from_url(url.url)
|
||||||
recipe: Recipe = db.recipes.create(session, recipe.dict())
|
recipe: Recipe = db.recipes.create(session, recipe.dict())
|
||||||
create_recipe_event("Recipe Created (URL)", f"'{recipe.name}' by {current_user.full_name}", session=session)
|
|
||||||
|
background_tasks.add_task(
|
||||||
|
create_recipe_event,
|
||||||
|
"Recipe Created (URL)",
|
||||||
|
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
|
||||||
|
session=session,
|
||||||
|
attachment=recipe.image_dir.joinpath("min-original.webp"),
|
||||||
|
)
|
||||||
|
|
||||||
return recipe.slug
|
return recipe.slug
|
||||||
|
|
||||||
@ -64,7 +81,6 @@ def update_recipe(
|
|||||||
""" Updates a recipe by existing slug and data. """
|
""" Updates a recipe by existing slug and data. """
|
||||||
|
|
||||||
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
|
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
|
||||||
print(recipe.assets)
|
|
||||||
|
|
||||||
check_assets(original_slug=recipe_slug, recipe=recipe)
|
check_assets(original_slug=recipe_slug, recipe=recipe)
|
||||||
|
|
||||||
@ -91,6 +107,7 @@ def patch_recipe(
|
|||||||
|
|
||||||
@router.delete("/{recipe_slug}")
|
@router.delete("/{recipe_slug}")
|
||||||
def delete_recipe(
|
def delete_recipe(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
recipe_slug: str,
|
recipe_slug: str,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
@ -100,7 +117,12 @@ def delete_recipe(
|
|||||||
try:
|
try:
|
||||||
recipe: Recipe = db.recipes.delete(session, recipe_slug)
|
recipe: Recipe = db.recipes.delete(session, recipe_slug)
|
||||||
delete_assets(recipe_slug=recipe_slug)
|
delete_assets(recipe_slug=recipe_slug)
|
||||||
create_recipe_event("Recipe Deleted", f"'{recipe.name}' deleted by {current_user.full_name}", session=session)
|
background_tasks.add_task(
|
||||||
|
create_recipe_event,
|
||||||
|
"Recipe Deleted",
|
||||||
|
f"'{recipe.name}' deleted by {current_user.full_name}",
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
return recipe
|
return recipe
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, Request, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, Request, status
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from mealie.core import security
|
from mealie.core import security
|
||||||
@ -15,6 +15,7 @@ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
|||||||
@router.post("/token/long")
|
@router.post("/token/long")
|
||||||
@router.post("/token")
|
@router.post("/token")
|
||||||
def get_token(
|
def get_token(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
request: Request,
|
request: Request,
|
||||||
data: OAuth2PasswordRequestForm = Depends(),
|
data: OAuth2PasswordRequestForm = Depends(),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -25,7 +26,9 @@ def get_token(
|
|||||||
user = authenticate_user(session, email, password)
|
user = authenticate_user(session, email, password)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
create_user_event("Failed Login", f"Username: {email}, Source IP: '{request.client.host}'")
|
background_tasks.add_task(
|
||||||
|
create_user_event, "Failed Login", f"Username: {email}, Source IP: '{request.client.host}'"
|
||||||
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from mealie.core import security
|
from mealie.core import security
|
||||||
from mealie.core.config import app_dirs, settings
|
from mealie.core.config import app_dirs, settings
|
||||||
@ -17,13 +17,16 @@ router = APIRouter(prefix="/api/users", tags=["Users"])
|
|||||||
|
|
||||||
@router.post("", response_model=UserOut, status_code=201)
|
@router.post("", response_model=UserOut, status_code=201)
|
||||||
async def create_user(
|
async def create_user(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
new_user: UserIn,
|
new_user: UserIn,
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
|
|
||||||
new_user.password = get_password_hash(new_user.password)
|
new_user.password = get_password_hash(new_user.password)
|
||||||
create_user_event("User Created", f"Created by {current_user.full_name}", session=session)
|
background_tasks.add_task(
|
||||||
|
create_user_event, "User Created", f"Created by {current_user.full_name}", session=session
|
||||||
|
)
|
||||||
return db.users.create(session, new_user.dict())
|
return db.users.create(session, new_user.dict())
|
||||||
|
|
||||||
|
|
||||||
@ -138,6 +141,7 @@ async def update_password(
|
|||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_user(
|
async def delete_user(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
id: int,
|
id: int,
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -150,6 +154,6 @@ async def delete_user(
|
|||||||
if current_user.id == id or current_user.admin:
|
if current_user.id == id or current_user.admin:
|
||||||
try:
|
try:
|
||||||
db.users.delete(session, id)
|
db.users.delete(session, id)
|
||||||
create_user_event("User Deleted", f"User ID: {id}", session=session)
|
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||||
from mealie.core.security import get_password_hash
|
from mealie.core.security import get_password_hash
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
@ -25,6 +25,7 @@ async def get_all_open_sign_ups(
|
|||||||
|
|
||||||
@router.post("", response_model=SignUpToken)
|
@router.post("", response_model=SignUpToken)
|
||||||
async def create_user_sign_up_key(
|
async def create_user_sign_up_key(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
key_data: SignUpIn,
|
key_data: SignUpIn,
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -39,12 +40,16 @@ async def create_user_sign_up_key(
|
|||||||
"name": key_data.name,
|
"name": key_data.name,
|
||||||
"admin": key_data.admin,
|
"admin": key_data.admin,
|
||||||
}
|
}
|
||||||
create_user_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
|
|
||||||
|
background_tasks.add_task(
|
||||||
|
create_user_event, "Sign-up Token Created", f"Created by {current_user.full_name}", session=session
|
||||||
|
)
|
||||||
return db.sign_ups.create(session, sign_up)
|
return db.sign_ups.create(session, sign_up)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{token}")
|
@router.post("/{token}")
|
||||||
async def create_user_with_token(
|
async def create_user_with_token(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
token: str,
|
token: str,
|
||||||
new_user: UserIn,
|
new_user: UserIn,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
@ -62,7 +67,9 @@ async def create_user_with_token(
|
|||||||
db.users.create(session, new_user.dict())
|
db.users.create(session, new_user.dict())
|
||||||
|
|
||||||
# DeleteToken
|
# DeleteToken
|
||||||
create_user_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
|
background_tasks.add_task(
|
||||||
|
create_user_event, "Sign-up Token Used", f"New User {new_user.full_name}", session=session
|
||||||
|
)
|
||||||
db.sign_ups.delete(session, token)
|
db.sign_ups.delete(session, token)
|
||||||
|
|
||||||
|
|
||||||
|
61
mealie/schema/event_notifications.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
|
||||||
|
class DeclaredTypes(str, Enum):
|
||||||
|
general = "General"
|
||||||
|
discord = "Discord"
|
||||||
|
gotify = "Gotify"
|
||||||
|
pushover = "Pushover"
|
||||||
|
home_assistant = "Home Assistant"
|
||||||
|
|
||||||
|
|
||||||
|
class EventNotificationOut(CamelModel):
|
||||||
|
id: Optional[int]
|
||||||
|
name: str = ""
|
||||||
|
type: DeclaredTypes = DeclaredTypes.general
|
||||||
|
general: bool = True
|
||||||
|
recipe: bool = True
|
||||||
|
backup: bool = True
|
||||||
|
scheduled: bool = True
|
||||||
|
migration: bool = True
|
||||||
|
group: bool = True
|
||||||
|
user: bool = True
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class EventNotificationIn(EventNotificationOut):
|
||||||
|
notification_url: str = ""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class Discord(CamelModel):
|
||||||
|
webhook_id: str
|
||||||
|
webhook_token: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_url(self) -> str:
|
||||||
|
return f"discord://{self.webhook_id}/{self.webhook_token}/"
|
||||||
|
|
||||||
|
|
||||||
|
class GotifyPriority(str, Enum):
|
||||||
|
low = "low"
|
||||||
|
moderate = "moderate"
|
||||||
|
normal = "normal"
|
||||||
|
high = "high"
|
||||||
|
|
||||||
|
|
||||||
|
class Gotify(CamelModel):
|
||||||
|
hostname: str
|
||||||
|
token: str
|
||||||
|
priority: GotifyPriority = GotifyPriority.normal
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_url(self) -> str:
|
||||||
|
return f"gotifys://{self.hostname}/{self.token}/?priority={self.priority}"
|
@ -30,3 +30,8 @@ class Event(CamelModel):
|
|||||||
class EventsOut(CamelModel):
|
class EventsOut(CamelModel):
|
||||||
total: int
|
total: int
|
||||||
events: list[Event]
|
events: list[Event]
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvent(CamelModel):
|
||||||
|
id: Optional[int]
|
||||||
|
test_url: Optional[str]
|
||||||
|
@ -1,23 +1,60 @@
|
|||||||
|
import apprise
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import create_session
|
from mealie.db.db_setup import create_session
|
||||||
from mealie.schema.events import Event, EventCategory
|
from mealie.schema.events import Event, EventCategory
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
|
||||||
def save_event(title, text, category, session: Session):
|
def test_notification(notification_url, event=None) -> bool:
|
||||||
|
|
||||||
|
if event is None:
|
||||||
|
event = Event(
|
||||||
|
title="Test Notification",
|
||||||
|
text="This is a test message from the Mealie API server",
|
||||||
|
category=EventCategory.general.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
post_notifications(event, [notification_url], hard_fail=True)
|
||||||
|
|
||||||
|
|
||||||
|
def post_notifications(event: Event, notification_urls=list[str], hard_fail=False, attachment=None):
|
||||||
|
asset = apprise.AppriseAsset(async_mode=False)
|
||||||
|
apobj = apprise.Apprise(asset=asset)
|
||||||
|
|
||||||
|
for dest in notification_urls:
|
||||||
|
status = apobj.add(dest)
|
||||||
|
|
||||||
|
if not status and hard_fail:
|
||||||
|
raise Exception("Apprise URL Add Failed")
|
||||||
|
|
||||||
|
print(attachment)
|
||||||
|
|
||||||
|
apobj.notify(
|
||||||
|
body=event.text,
|
||||||
|
title=event.title,
|
||||||
|
attach=str(attachment),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_event(title, text, category, session: Session, attachment=None):
|
||||||
event = Event(title=title, text=text, category=category)
|
event = Event(title=title, text=text, category=category)
|
||||||
session = session or create_session()
|
session = session or create_session()
|
||||||
db.events.create(session, event.dict())
|
db.events.create(session, event.dict())
|
||||||
|
|
||||||
|
notification_objects = db.event_notifications.get(session=session, match_value=True, match_key=category, limit=9999)
|
||||||
|
notification_urls = [x.notification_url for x in notification_objects]
|
||||||
|
post_notifications(event, notification_urls, attachment=attachment)
|
||||||
|
|
||||||
|
|
||||||
def create_general_event(title, text, session=None):
|
def create_general_event(title, text, session=None):
|
||||||
category = EventCategory.general
|
category = EventCategory.general
|
||||||
save_event(title=title, text=text, category=category, session=session)
|
save_event(title=title, text=text, category=category, session=session)
|
||||||
|
|
||||||
|
|
||||||
def create_recipe_event(title, text, session=None):
|
def create_recipe_event(title, text, session=None, attachment=None):
|
||||||
category = EventCategory.recipe
|
category = EventCategory.recipe
|
||||||
save_event(title=title, text=text, category=category, session=session)
|
|
||||||
|
save_event(title=title, text=text, category=category, session=session, attachment=attachment)
|
||||||
|
|
||||||
|
|
||||||
def create_backup_event(title, text, session=None):
|
def create_backup_event(title, text, session=None):
|
||||||
|
95
poetry.lock
generated
@ -22,6 +22,23 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "apprise"
|
||||||
|
version = "0.9.2"
|
||||||
|
description = "Push Notifications that work with just about every platform!"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=5.0"
|
||||||
|
cryptography = "*"
|
||||||
|
markdown = "*"
|
||||||
|
PyYAML = "*"
|
||||||
|
requests = "*"
|
||||||
|
requests-oauthlib = "*"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "apscheduler"
|
name = "apscheduler"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
@ -189,6 +206,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["toml"]
|
toml = ["toml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "3.4.7"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.12"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
||||||
|
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||||
|
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||||
|
sdist = ["setuptools-rust (>=0.11.4)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
version = "5.0.7"
|
version = "5.0.7"
|
||||||
@ -464,7 +500,7 @@ source = ["Cython (>=0.29.7)"]
|
|||||||
name = "markdown"
|
name = "markdown"
|
||||||
version = "3.3.4"
|
version = "3.3.4"
|
||||||
description = "Python implementation of Markdown."
|
description = "Python implementation of Markdown."
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
@ -573,6 +609,19 @@ plot = ["matplotlib"]
|
|||||||
tgrep = ["pyparsing"]
|
tgrep = ["pyparsing"]
|
||||||
twitter = ["twython"]
|
twitter = ["twython"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oauthlib"
|
||||||
|
version = "3.1.0"
|
||||||
|
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
rsa = ["cryptography"]
|
||||||
|
signals = ["blinker"]
|
||||||
|
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "20.9"
|
version = "20.9"
|
||||||
@ -916,6 +965,21 @@ urllib3 = ">=1.21.1,<1.27"
|
|||||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-oauthlib"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "OAuthlib authentication support for Requests."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
oauthlib = ">=3.0.0"
|
||||||
|
requests = ">=2.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "4.7.2"
|
version = "4.7.2"
|
||||||
@ -1172,7 +1236,7 @@ python-versions = "*"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "bfdb4d3d5d69e53f16b315f993b712a703058d3f59e24644681ccc9062cf5143"
|
content-hash = "73bac73c62e64c90a29816dde9ef1d896e8ca0b4271e67cde6ca8cc56bd87efd"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1187,6 +1251,10 @@ appdirs = [
|
|||||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||||
]
|
]
|
||||||
|
apprise = [
|
||||||
|
{file = "apprise-0.9.2-py2.py3-none-any.whl", hash = "sha256:e3f34fcf7cd717704f04b194f6fd62719e563b7d338415467e0cd0fbd7fc5e61"},
|
||||||
|
{file = "apprise-0.9.2.tar.gz", hash = "sha256:263bd6ed81cb33f3d24c9353506921129890a654965fbe9014f7f562bf2ba9f9"},
|
||||||
|
]
|
||||||
apscheduler = [
|
apscheduler = [
|
||||||
{file = "APScheduler-3.7.0-py2.py3-none-any.whl", hash = "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3"},
|
{file = "APScheduler-3.7.0-py2.py3-none-any.whl", hash = "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3"},
|
||||||
{file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"},
|
{file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"},
|
||||||
@ -1329,6 +1397,20 @@ coverage = [
|
|||||||
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
|
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
|
||||||
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
|
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
|
||||||
]
|
]
|
||||||
|
cryptography = [
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"},
|
||||||
|
{file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
|
||||||
|
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
|
||||||
|
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
|
||||||
|
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
|
||||||
|
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
|
||||||
|
{file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
|
||||||
|
]
|
||||||
decorator = [
|
decorator = [
|
||||||
{file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
|
{file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
|
||||||
{file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
|
{file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
|
||||||
@ -1617,6 +1699,10 @@ nltk = [
|
|||||||
{file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"},
|
{file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"},
|
||||||
{file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"},
|
{file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"},
|
||||||
]
|
]
|
||||||
|
oauthlib = [
|
||||||
|
{file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
|
||||||
|
{file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
|
||||||
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
||||||
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
||||||
@ -1854,6 +1940,11 @@ requests = [
|
|||||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||||
]
|
]
|
||||||
|
requests-oauthlib = [
|
||||||
|
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
|
||||||
|
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
|
||||||
|
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
|
||||||
|
]
|
||||||
rsa = [
|
rsa = [
|
||||||
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
|
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
|
||||||
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
|
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
|
||||||
|
@ -32,6 +32,7 @@ passlib = "^1.7.4"
|
|||||||
lxml = "4.6.2"
|
lxml = "4.6.2"
|
||||||
Pillow = "^8.2.0"
|
Pillow = "^8.2.0"
|
||||||
pathvalidate = "^2.4.1"
|
pathvalidate = "^2.4.1"
|
||||||
|
apprise = "^0.9.2"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|