├── LICENSE ├── README.md ├── bin ├── ta ├── taskdb_backup_cleanup ├── taskdb_backup_db ├── taskdb_cal_export ├── taskdb_cal_export_all ├── taskdb_cal_import ├── taskdb_cal_sync ├── taskdb_cal_sync_conflict_resolve ├── taskdb_cal_sync_loop ├── taskdb_cal_sync_wait ├── taskdb_create ├── taskdb_dump_all ├── taskdb_dump_data ├── taskdb_dump_schema ├── taskdb_git_commit_data_dump ├── taskdb_listen_changes ├── taskdb_render ├── taskdb_render_query_gen ├── taskdb_report ├── taskdb_tree_mermaid ├── taskdb_tree_view ├── tdone ├── tdt ├── tdtnw ├── tdtw ├── tea └── tlog ├── doc ├── demo │ ├── configs │ │ ├── psqlrc │ │ ├── taskdb_cal_sync_setup │ │ └── vdirsyncer │ ├── datasets │ │ ├── fictional_task.psql_copy │ │ ├── sleep.psql_copy │ │ └── workout.psql_copy │ └── docker │ │ ├── Dockerfile.gentoo │ │ ├── Makefile │ │ ├── gentoo │ │ └── etc │ │ │ ├── local.d │ │ │ └── omnidb.start │ │ │ └── portage │ │ │ ├── make.conf │ │ │ ├── package.accept_keywords │ │ │ └── package.use │ │ └── launch-container └── taskdb_logo.png └── share ├── auto-tagging ├── edit_and_exec ├── run_all ├── suggest └── suggestions │ ├── 01_completed_cleanup │ ├── cond.sql.inc │ └── set.sql.inc │ ├── 10_link_to_activity_by_descr_match │ ├── cond.sql.inc │ └── set.sql.inc │ ├── 19_extract_alias_from_descr │ ├── cond.sql.inc │ └── set.sql.inc │ ├── 20_link_to_parent_by_alias_in_project │ ├── cond.sql.inc │ └── set.sql.inc │ └── 30_link_to_parent_by_alias_in_descr_prefix │ ├── cond.sql.inc │ └── set.sql.inc ├── functions ├── alias.sql ├── depgraph.sql ├── depgraph_root_to_selection.sql ├── deps.sql ├── graph.sql ├── graph_node_repr.sql ├── priority_level.sql └── rdeps.sql ├── schema.sql ├── schema_changes ├── 20181001-reorder_drop_cols.sql ├── 20181004-change-tdt.sql ├── 20190201-revert-change-tdt.sql ├── 20190202-duration-integer.sql ├── 20190218-add-tdtnw.sql ├── 20190404-add-megatasks-view.sql ├── 20190405-add-overdue-view.sql ├── 20190406-add-conflicting-view.sql ├── 20190501-trigger-preserve-modified-if-given.sql ├── 20190502-trigger-preserve-modified-fix.sql ├── 20191001-enable-grafana-access.sql ├── 20191002-megatasks-view-include-unscheduled.sql ├── 20191003-priority-text.sql ├── 2020-04-02-0001-improve-graph.sql ├── 2020-04-02-0002-actualize-megatasks-view.sql ├── 2020-04-02-0003-add-alias-fn.sql ├── 2020-04-02-0004-notify-payload-truncate.sql ├── 20200229-add-alias-field.sql ├── 20200301-add-depgraph-fn.sql ├── 20200301-add-graph-fn.sql ├── 20200302-fix-graph-fn.sql ├── 20200303-add-depgraph_root_to_selection-fn.sql └── 20200303-fix-graph-fn.sql ├── snippets └── tree_view.sql ├── triggers └── changes_notify_fn.sql └── views ├── conflicting.sql ├── megatasks.sql ├── overdue.sql └── tdt_all.sql /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Please visit project wiki](https://github.com/andrey-utkin/taskdb/wiki) 2 | -------------------------------------------------------------------------------- /bin/ta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Add new task to DB""" 4 | 5 | import os 6 | import pathlib 7 | import sys 8 | import psycopg2 9 | import psycopg2.extras 10 | 11 | description = sys.argv[1] 12 | if len(sys.argv) > 2: 13 | project = sys.argv[2] 14 | else: 15 | project = None 16 | 17 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 18 | conn_string = file.read() 19 | 20 | conn = psycopg2.connect(conn_string) 21 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 22 | cur.execute("PREPARE INS_FROM_TA AS INSERT INTO tasks (scheduled, duration, description, project) VALUES(date_trunc('hour', CURRENT_TIMESTAMP), 3600, $1, $2) RETURNING uuid"); 23 | 24 | cur.execute("EXECUTE INS_FROM_TA (%s, %s)", (description, project)) 25 | conn.commit() 26 | row = cur.fetchone() 27 | print(row['uuid']) 28 | 29 | LAST_UUID_FILE_PATH = str(pathlib.Path.home()) + '/.taskdb/last_uuid' 30 | with open(LAST_UUID_FILE_PATH, 'w') as f: 31 | f.write(row['uuid']) 32 | -------------------------------------------------------------------------------- /bin/taskdb_backup_cleanup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | BKP_LOCATION=/var/backups/taskdb 5 | BKP_AGE=15 # days 6 | 7 | pushd $BKP_LOCATION >/dev/null 8 | 9 | find . -mindepth 1 -mtime +$BKP_AGE -delete 10 | -------------------------------------------------------------------------------- /bin/taskdb_backup_db: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | BKP_LOCATION=/var/backups/taskdb 6 | 7 | mkdir -p $BKP_LOCATION 8 | chmod u=rwx,g=,o= $BKP_LOCATION 9 | pushd $BKP_LOCATION >/dev/null 10 | 11 | DATABASES=$( 12 | echo -e '\pset format unaligned\n \pset tuples_only\n SELECT datname FROM pg_database;' \ 13 | | psql -Upostgres --quiet \ 14 | | grep -v '^\(postgres\|template[01]\)$' 15 | ) 16 | 17 | DATE=$(date +%FT%H:%M:%S) 18 | 19 | for dbname in $DATABASES 20 | do 21 | FILENAME="${dbname}_$DATE" 22 | pg_dump -Upostgres $dbname | xz -zce > "${FILENAME}.sql.xz" 23 | done 24 | 25 | pg_basebackup \ 26 | -D - -F tar --gzip \ 27 | --wal-method=fetch --write-recovery-conf \ 28 | --verbose \ 29 | --label="taskdb basebackup $DATE" \ 30 | -Upostgres \ 31 | > basebackup_"$DATE".tar.gz 32 | -------------------------------------------------------------------------------- /bin/taskdb_cal_export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Generate .ics file for a task""" 3 | 4 | import os 5 | import sys 6 | import subprocess 7 | import psycopg2 8 | import psycopg2.extras 9 | from datetime import datetime, timedelta 10 | import pytz 11 | from icalendar import Calendar, Event, Alarm 12 | from icalendar import vCalAddress, vText 13 | 14 | 15 | uuid = sys.argv[1] 16 | 17 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 18 | conn_string = file.read() 19 | 20 | conn = psycopg2.connect(conn_string) 21 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 22 | cur.execute("PREPARE SEL_FROM_CAL_EXPORT AS SELECT * FROM tasks WHERE uuid = $1"); 23 | 24 | cur.execute("EXECUTE SEL_FROM_CAL_EXPORT (%s)", (uuid,)) 25 | row = cur.fetchone() 26 | if not row: 27 | print('Failed to query the task.') 28 | sys.exit(1) 29 | 30 | cal = Calendar() 31 | 32 | # Some properties are required to be compliant: 33 | cal.add('prodid', '-//andrey-utkin//taskdb//') 34 | cal.add('version', '2.0') 35 | 36 | event = Event() 37 | event.add('summary', row['description']) 38 | if row['annotation']: 39 | event.add('description', row['annotation']) 40 | 41 | assert row['scheduled'] 42 | 43 | # Produce local timezone dates in calendar entry. 44 | # Otherwise editing the event in apps is a pain: 45 | # user picks time, but it's UTC time. 46 | with open('/etc/timezone', 'r') as tzfile: 47 | tzname = tzfile.read().strip() 48 | tz = pytz.timezone(tzname) 49 | scheduled_in_tz = row['scheduled'].astimezone(tz) 50 | 51 | event.add('dtstart', scheduled_in_tz) 52 | 53 | if row['duration'] and row['duration'] != '': 54 | dtend = row['scheduled'] + timedelta(seconds=int(row['duration'])) 55 | event.add('dtend', dtend.astimezone(tz)) 56 | 57 | event.add('dtstamp', row['modified'].astimezone(pytz.utc)) 58 | 59 | event['uid'] = row['uuid'] 60 | 61 | alarm = Alarm() 62 | alarm['description'] = row['description'] 63 | alarm['trigger'] = '-PT10M' 64 | alarm['action'] = 'DISPLAY' 65 | event.add_component(alarm) 66 | 67 | cal.add_component(event) 68 | 69 | # taskdb calendar dir path 70 | directory = '{home}/.taskdb/calendar'.format(home=os.getenv("HOME")) 71 | f = open(os.path.join(directory, row['uuid'] + '.ics'), 'wb') 72 | f.write(cal.to_ical()) 73 | f.close() 74 | -------------------------------------------------------------------------------- /bin/taskdb_cal_export_all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | from pprint import pprint 6 | import psycopg2 7 | import psycopg2.extras 8 | from datetime import datetime 9 | import icalendar 10 | 11 | mydir = os.path.dirname(__file__) 12 | mydir = os.path.abspath(mydir) 13 | mydir += '/' 14 | cal_path = '{home}/.taskdb/calendar'.format(home=os.getenv("HOME")) 15 | 16 | def existing_file_uptodate(row): 17 | filepath = os.path.join(cal_path, row['uuid'] + '.ics') 18 | if not os.path.exists(filepath): 19 | return False 20 | icalfile = open(filepath, 'rb') 21 | cal = icalendar.Calendar.from_ical(icalfile.read()) 22 | for component in cal.walk(): 23 | if component.name == "VEVENT": 24 | dtstamp = component.get('dtstamp').dt 25 | break 26 | 27 | rounded_modified = row['modified'].replace(microsecond=0) 28 | 29 | return dtstamp == rounded_modified 30 | 31 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 32 | conn_string = file.read() 33 | 34 | conn = psycopg2.connect(conn_string) 35 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 36 | 37 | cur.execute("SELECT uuid, modified FROM tasks WHERE ((tasks.status = 'pending'::public.task_status) AND (tasks.scheduled IS NOT NULL));") 38 | 39 | rows = cur.fetchall() 40 | for row in rows: 41 | if existing_file_uptodate(row): 42 | continue 43 | child = subprocess.Popen(['/usr/bin/python3', mydir + 'taskdb_cal_export', row['uuid']]) 44 | child.wait() 45 | 46 | cur.close() 47 | conn.close() 48 | 49 | # Delete files for entries which are no longer in the list. 50 | stale_files = [] 51 | with os.scandir(cal_path) as it: 52 | for entry in it: 53 | if not entry.name.endswith('.ics'): 54 | continue 55 | found = False 56 | for row in rows: 57 | if entry.name.startswith(row['uuid']): 58 | found = True 59 | break 60 | if not found: 61 | stale_files.append(entry.name) 62 | 63 | for stale in stale_files: 64 | print('Removing stale file {}'.format(stale)) 65 | os.unlink(os.path.join(cal_path, stale)) 66 | -------------------------------------------------------------------------------- /bin/taskdb_cal_import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import subprocess 6 | import datetime 7 | import dateutil.parser 8 | import pytz 9 | import psycopg2 10 | import psycopg2.extras 11 | import icalendar 12 | 13 | # Insert or update? 14 | action = sys.argv[1] 15 | 16 | # Locate ical file 17 | uuid = sys.argv[2] 18 | filepath = '{home}/.taskdb/calendar/{uuid}.ics'.format(home=os.getenv("HOME"), uuid=uuid) 19 | 20 | # Parse it 21 | # https://gist.github.com/meskarune/63600e64df56a607efa211b9a87fb443 22 | icalfile = open(filepath, 'rb') 23 | cal = icalendar.Calendar.from_ical(icalfile.read()) 24 | 25 | for component in cal.walk(): 26 | if component.name == "VEVENT": 27 | description = component.get('summary') 28 | annotation = component.get('description') 29 | #location = component.get('location') 30 | scheduled = component.get('dtstart').dt 31 | dtend = component.get('dtend') 32 | duration_component = component.get('duration') 33 | duration = None 34 | if dtend: 35 | duration = int((dtend.dt - scheduled).total_seconds()) 36 | elif duration_component: 37 | duration = int(duration_component.dt.total_seconds()) 38 | dtstamp = component.get('dtstamp').dt 39 | break 40 | 41 | icalfile.close() 42 | 43 | stuff = (uuid, scheduled, duration, description, annotation, dtstamp) 44 | print(stuff) 45 | 46 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 47 | conn_string = file.read() 48 | 49 | conn = psycopg2.connect(conn_string) 50 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 51 | 52 | # Depending on $1, insert or update into DB 53 | if action == 'insert': 54 | cur.execute("PREPARE INS_FROM_CAL_IMPORT AS INSERT INTO tasks (uuid, scheduled, duration, description, annotation, entry, modified) VALUES($1, $2, $3, $4, $5, $6, $6)") 55 | cur.execute("EXECUTE INS_FROM_CAL_IMPORT (%s, %s, %s, %s, %s, %s)", stuff) 56 | else: 57 | # This won't modify a row if it has modification time later than imported object. 58 | cur.execute("PREPARE UPD_FROM_CAL_IMPORT AS UPDATE tasks SET scheduled = $2, duration = $3, description = $4, annotation = $5, modified = $6 WHERE uuid = $1 AND modified < $6") 59 | cur.execute("EXECUTE UPD_FROM_CAL_IMPORT (%s, %s, %s, %s, %s, %s)", stuff) 60 | print('Rows updated: %d' % cur.rowcount) 61 | 62 | conn.commit() 63 | -------------------------------------------------------------------------------- /bin/taskdb_cal_sync: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | MYDIR=$(readlink -f $(dirname $0)) 6 | 7 | CAL_DIR="$HOME/.taskdb/calendar" 8 | CAL_UUID=$(basename $(readlink -f "$CAL_DIR")) 9 | 10 | pushd "$CAL_DIR" > /dev/null || exit 1 11 | 12 | if [[ -d .git ]]; then 13 | git add . 14 | git commit --quiet --all -m "Orphan changes before us refilling the dir from DB" || true 15 | fi 16 | 17 | "$MYDIR"/taskdb_cal_export_all 18 | 19 | if [[ -d .git ]]; then 20 | git add . 21 | git commit --quiet --all -m "Changes after us refilling the dir from DB" || true 22 | fi 23 | 24 | SYNC_LOG_FILE="$HOME/.taskdb/vdirsyncer.out" 25 | vdirsyncer sync "fastmail_calendar/$CAL_UUID" &> "$SYNC_LOG_FILE" 26 | cat "$SYNC_LOG_FILE" 27 | 28 | if [[ -d .git ]]; then 29 | git add . 30 | (echo -e "Changes after 'vdirsyncer sync'\n"; 31 | cat "$SYNC_LOG_FILE" 32 | ) | git commit --all --file=- 33 | fi 34 | 35 | # Look for lines like 36 | # "Copying (updating) item 2bb6df40-0af5-4f76-acf0-d3fae801dca1 to fastmail_calendar_local/fb9ed04e-572b-477f-bdad-d6dbb32c0d82" 37 | UUIDS=$(grep "Copying (updating) .* to .*_local/" "$SYNC_LOG_FILE" \ 38 | | cut -d ' ' -f 4) 39 | # Parse and update in DB 40 | for uuid in $UUIDS; do 41 | "$MYDIR/taskdb_cal_import" update "$uuid" 42 | sleep 2 # to allow taskdb_listen_changes to pick up individual task changes not mixed together 43 | done 44 | 45 | # Look for lines like 46 | # "Copying (uploading) item 2bb6df40-0af5-4f76-acf0-d3fae801dca1 to fastmail_calendar_local/fb9ed04e-572b-477f-bdad-d6dbb32c0d82" 47 | UUIDS=$(grep "Copying (uploading) .* to .*_local/" "$SYNC_LOG_FILE" \ 48 | | cut -d ' ' -f 4) 49 | # Parse and insert into DB 50 | for uuid in $UUIDS; do 51 | "$MYDIR/taskdb_cal_import" insert "$uuid" 52 | sleep 2 # to allow taskdb_listen_changes to pick up individual task changes not mixed together 53 | done 54 | 55 | # Look for lines like 56 | # "Deleting item 2bb6df40-0af5-4f76-acf0-d3fae801dca1 from fastmail_calendar_local/fb9ed04e-572b-477f-bdad-d6dbb32c0d82" 57 | # which mean the item has been removed in some external calendar app. 58 | # The most convenient reaction is to mark the item "deleted" in the database. 59 | # Note: before, the action here was to mark the item "completed", but since 60 | # then I mark the items completed in bulk with "auto-tagging", while deleting 61 | # an unneeded item currently requires a manual database edit. 62 | UUIDS=$(grep "Deleting .* from .*_local/" "$SYNC_LOG_FILE" \ 63 | | cut -d ' ' -f 3) 64 | # Parse and update in DB 65 | for uuid in $UUIDS; do 66 | echo "UPDATE tasks SET status = 'deleted' WHERE uuid = '$uuid';" | psql 67 | sleep 2 # to allow taskdb_listen_changes to pick up individual task changes not mixed together 68 | done 69 | 70 | if [[ -d .git ]]; then 71 | git push --quiet 72 | fi 73 | -------------------------------------------------------------------------------- /bin/taskdb_cal_sync_conflict_resolve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import icalendar 6 | import shutil 7 | 8 | def get_dtstamp(filepath): 9 | if not os.path.exists(filepath): 10 | return None 11 | icalfile = open(filepath, 'rb') 12 | cal = icalendar.Calendar.from_ical(icalfile.read()) 13 | for component in cal.walk(): 14 | if component.name == "VEVENT": 15 | return component.get('dtstamp').dt 16 | return None 17 | 18 | A_FILEPATH = sys.argv[1] 19 | B_FILEPATH = sys.argv[2] 20 | 21 | a_dtstamp = get_dtstamp(A_FILEPATH) 22 | b_dtstamp = get_dtstamp(B_FILEPATH) 23 | 24 | if a_dtstamp > b_dtstamp: 25 | newer = A_FILEPATH 26 | older = B_FILEPATH 27 | else: 28 | newer = B_FILEPATH 29 | older = A_FILEPATH 30 | 31 | shutil.copyfile(newer, older) 32 | -------------------------------------------------------------------------------- /bin/taskdb_cal_sync_loop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MYDIR=$(dirname $0) 4 | while true 5 | do 6 | date 7 | "$MYDIR"/taskdb_cal_sync 8 | sleep 1m 9 | done 10 | -------------------------------------------------------------------------------- /bin/taskdb_cal_sync_wait: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while pgrep --full --list-full --exact '/bin/bash /usr/bin/taskdb_cal_sync' >/dev/null 4 | do 5 | echo -n . 6 | sleep 1 7 | done 8 | -------------------------------------------------------------------------------- /bin/taskdb_create: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | USERNAME=$(whoami) 5 | 6 | createuser "$USERNAME" -Upostgres 7 | createdb "$USERNAME" -Upostgres --owner="$USERNAME" 8 | createuser taskdb_grafana -Upostgres 9 | echo 'CREATE EXTENSION "uuid-ossp"' | psql -Upostgres "$USERNAME" 10 | psql < /usr/share/taskdb-9999/schema.sql 11 | 12 | mkdir ~/.taskdb 13 | cd ~/.taskdb 14 | echo "dbname=$USERNAME user=$USERNAME" > conn_string_py 15 | -------------------------------------------------------------------------------- /bin/taskdb_dump_all: -------------------------------------------------------------------------------- 1 | pg_dump \ 2 | ; 3 | -------------------------------------------------------------------------------- /bin/taskdb_dump_data: -------------------------------------------------------------------------------- 1 | pg_dump \ 2 | --no-owner \ 3 | --data-only \ 4 | ; 5 | -------------------------------------------------------------------------------- /bin/taskdb_dump_schema: -------------------------------------------------------------------------------- 1 | pg_dump \ 2 | --no-owner \ 3 | --schema-only \ 4 | ; 5 | -------------------------------------------------------------------------------- /bin/taskdb_git_commit_data_dump: -------------------------------------------------------------------------------- 1 | set -e 2 | MYPATH=$(readlink -f $(dirname $0)) 3 | pushd ~/.taskdb/dump.git > /dev/null 4 | "$MYPATH"/taskdb_dump_all > db.sql 5 | git commit --file=- --all 6 | git push || true 7 | -------------------------------------------------------------------------------- /bin/taskdb_listen_changes: -------------------------------------------------------------------------------- 1 | MYPATH=$(dirname $0) 2 | 3 | while read -r LINE; do 4 | \ 5 | if [[ "$LINE" =~ 'Asynchronous notification' ]]; then 6 | echo "$LINE" 7 | PAYLOAD=${LINE##*payload \"} 8 | PAYLOAD=${PAYLOAD%%\" received*} 9 | # FIXME turns also literal \n into newlines 10 | echo "$PAYLOAD" | sed 's/\\n/\n/g' | "$MYPATH"/taskdb_git_commit_data_dump 11 | fi 12 | done < <( 13 | ( 14 | echo 'LISTEN "CHANGES";' 15 | while true; do 16 | echo 'select 1;' 17 | sleep 1 18 | done 19 | ) \ 20 | | psql \ 21 | ; 22 | ) 23 | -------------------------------------------------------------------------------- /bin/taskdb_render: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | aspect=$1 4 | timeline=$2 5 | 6 | render() { 7 | QUERY_SCRIPT=$1 8 | IMAGE=$2 9 | dot \ 10 | -Goverlap=false \ 11 | -Gsplines=true \ 12 | -Grankdir=LR \ 13 | -Tsvg \ 14 | <(psql -qtAX -f "$QUERY_SCRIPT") \ 15 | -o /var/www/taskdb/htdocs/"$IMAGE" 16 | } 17 | 18 | mydir=$(dirname "$0") 19 | render <("$mydir"/taskdb_render_query_gen "$aspect" "$timeline") "${aspect}_${timeline}.svg" 20 | -------------------------------------------------------------------------------- /bin/taskdb_render_query_gen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | 6 | # undo or personal 7 | aspect = sys.argv[1] 8 | # today, tomorrow, thisweek, nextweek, allpending 9 | timeframe = sys.argv[2] 10 | 11 | root = aspect 12 | exclusions = [] 13 | if aspect == 'personal': 14 | root = 'root' 15 | exclusions = ['undo'] 16 | 17 | status_condition = "status IN ('pending', 'completed')" 18 | if timeframe == 'allpending': 19 | status_condition = "status = 'pending'" 20 | 21 | subset_condition = "AND uuid IN (SELECT uuid FROM depgraph(alias('{root}')))".format(root=root) 22 | for exclusion in exclusions: 23 | subset_condition += "\n AND uuid NOT IN (SELECT uuid FROM depgraph(alias('{exclusion}')))".format(exclusion=exclusion) 24 | 25 | time_condition = '' 26 | if timeframe == 'yesterday': 27 | time_condition = ''' 28 | AND scheduled BETWEEN date_trunc('day', CURRENT_DATE) - INTERVAL '1 days' 29 | AND date_trunc('day', CURRENT_DATE) 30 | ''' 31 | elif timeframe == 'today': 32 | time_condition = ''' 33 | AND scheduled BETWEEN date_trunc('day', CURRENT_DATE) 34 | AND date_trunc('day', CURRENT_DATE) + INTERVAL '1 days' 35 | ''' 36 | elif timeframe == 'tomorrow': 37 | time_condition = ''' 38 | AND scheduled BETWEEN date_trunc('day', CURRENT_DATE) + INTERVAL '1 days' 39 | AND date_trunc('day', CURRENT_DATE) + INTERVAL '2 days' 40 | ''' 41 | elif timeframe in 'prevweek': 42 | time_condition = ''' 43 | AND scheduled BETWEEN date_trunc('week', CURRENT_DATE) - INTERVAL '1 weeks' 44 | AND date_trunc('week', CURRENT_DATE) 45 | ''' 46 | elif timeframe in 'week-before-prev': 47 | time_condition = ''' 48 | AND scheduled BETWEEN date_trunc('week', CURRENT_DATE) - INTERVAL '2 weeks' 49 | AND date_trunc('week', CURRENT_DATE) - INTERVAL '1 weeks' 50 | ''' 51 | elif timeframe == 'thisweek': 52 | time_condition = ''' 53 | AND scheduled BETWEEN date_trunc('week', CURRENT_DATE) 54 | AND date_trunc('week', CURRENT_DATE) + INTERVAL '1 weeks' 55 | ''' 56 | elif timeframe == 'nextweek': 57 | time_condition = ''' 58 | AND scheduled BETWEEN date_trunc('week', CURRENT_DATE) + INTERVAL '1 weeks' 59 | AND date_trunc('week', CURRENT_DATE) + INTERVAL '2 weeks' 60 | ''' 61 | elif timeframe in ['all', 'allpending']: 62 | time_condition = '' 63 | else: 64 | raise Exception('Unknown timeframe') 65 | 66 | extra_condition = 'AND NOT (scheduled IS NULL AND COALESCE(duration, 0) = 0)' 67 | if '--include-unscheduled-and-empty' in sys.argv: 68 | extra_condition = '' 69 | 70 | query = ''' 71 | SELECT value FROM graph(( 72 | SELECT ARRAY_AGG(depgraph_root_to_selection) FROM depgraph_root_to_selection 73 | ( 74 | (alias('{root}')), 75 | ( 76 | SELECT array_agg(tasks) FROM tasks 77 | WHERE 78 | {status_condition} 79 | {subset_condition} 80 | {time_condition} 81 | {extra_condition} 82 | ) 83 | ) 84 | )) 85 | ORDER BY order_ 86 | '''.format( 87 | root=root, 88 | status_condition=status_condition, 89 | subset_condition=subset_condition, 90 | time_condition=time_condition, 91 | extra_condition=extra_condition 92 | ) 93 | 94 | print(query) 95 | -------------------------------------------------------------------------------- /bin/taskdb_report: -------------------------------------------------------------------------------- 1 | FILTER=$1 2 | psql \ 3 | --quiet \ 4 | -c '\x on' \ 5 | -c '\pset format wrapped' \ 6 | -c '\pset pager off' \ 7 | -c "select * from ${FILTER}_report" \ 8 | ; 9 | 10 | -------------------------------------------------------------------------------- /bin/taskdb_tree_mermaid: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import subprocess 6 | import psycopg2 7 | import psycopg2.extras 8 | from datetime import datetime, timedelta 9 | import pytz 10 | from pprint import pprint 11 | 12 | 13 | TOP_UUID = sys.argv[1] 14 | TOP_CODENAME = sys.argv[2] if len(sys.argv) > 2 else None 15 | 16 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 17 | conn_string = file.read() 18 | 19 | conn = psycopg2.connect(conn_string) 20 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 21 | cur.execute("PREPARE SEL_BY_UUID_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE uuid = $1 AND status != 'deleted'"); 22 | cur.execute("PREPARE SEL_TOP_BY_CODENAME_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE (description ~ ('^' || $1 || '.*[(]TOP[)]$')) AND parent IS NULL AND status != 'deleted'"); 23 | cur.execute("PREPARE SEL_ALL_BY_CODENAME_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE (description ~ ('^' || $1)) AND status != 'deleted'"); 24 | cur.execute(""" 25 | PREPARE SEL_BY_PARENT_FROM_TREE_EDIT AS 26 | SELECT * FROM tasks 27 | WHERE 28 | ( 29 | parent = $1 30 | OR 31 | ( 32 | uuid::text IN 33 | ( 34 | SELECT UNNEST(STRING_TO_ARRAY(dependencies, '\n')) 35 | FROM tasks 36 | WHERE uuid=$1 37 | ) 38 | ) 39 | ) 40 | AND 41 | status not in ('deleted', 'cancelled') 42 | ORDER BY scheduled 43 | """); 44 | # Pending only: 45 | #cur.execute("PREPARE SEL_BY_PARENT_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE parent = $1 AND status = 'pending' ORDER BY scheduled"); 46 | 47 | def get_top_by_codename(codename): 48 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 49 | cur.execute("EXECUTE SEL_TOP_BY_CODENAME_FROM_TREE_EDIT (%s)", (codename,)) 50 | row = cur.fetchone() 51 | return row 52 | 53 | def get_by_uuid(uuid): 54 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 55 | cur.execute("EXECUTE SEL_BY_UUID_FROM_TREE_EDIT (%s)", (uuid,)) 56 | row = cur.fetchone() 57 | return row 58 | 59 | def get_by_parent(uuid): 60 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 61 | cur.execute("EXECUTE SEL_BY_PARENT_FROM_TREE_EDIT (%s)", (uuid,)) 62 | rows = cur.fetchall() 63 | return rows 64 | 65 | def get_children_recursively(uuid): 66 | children_db_rows = get_by_parent(uuid) 67 | children_list = [] 68 | for row in children_db_rows: 69 | child = dict() 70 | child['node'] = row 71 | child['children'] = get_children_recursively(row['uuid']) 72 | children_list.append(child) 73 | return children_list 74 | 75 | def status_str(row_status): 76 | if row_status == 'pending': 77 | return ' '; 78 | if row_status == 'completed': 79 | return 'V'; 80 | else: 81 | return 'Z'; 82 | 83 | 84 | def earliest_deep_subtask_starts_at(tree, earliest_starttime_so_far): 85 | 86 | if tree['node']['scheduled']: 87 | if not earliest_starttime_so_far or tree['node']['scheduled'] < earliest_starttime_so_far: 88 | earliest_starttime_so_far = tree['node']['scheduled'] 89 | 90 | for subtask in tree['children']: 91 | earliest_starttime_so_far = earliest_deep_subtask_starts_at(subtask, earliest_starttime_so_far) 92 | 93 | return earliest_starttime_so_far 94 | 95 | 96 | def print_tree(tree, depth=0): 97 | row = tree['node'] 98 | if row['scheduled']: 99 | sched_str = row['scheduled'].strftime('%m-%d %H:%M') 100 | else: 101 | sched_str = '-' 102 | if depth == 0: 103 | print('gantt') 104 | print('title {title}'.format(title=row['description'].replace(' (TOP)', ''))) 105 | 106 | status = [] 107 | if row['status'] == 'completed': 108 | status.append('done') 109 | 110 | if tree['children']: 111 | scheduled = earliest_deep_subtask_starts_at(tree, None) 112 | # Should be scheduled at the time of completion of latest subtask. 113 | # If not, validation function will warn. 114 | ends_at = row['scheduled'] 115 | else: 116 | scheduled = row['scheduled'] 117 | ends_at = 1 * (row['duration'] if row['duration'] is not None else 0) 118 | ends_at = str(ends_at) + 's' 119 | 120 | status = ', '.join(status) 121 | if status: 122 | status += ',' 123 | 124 | print(' {description} :{status} {scheduled}, {duration}'.format( 125 | description=row['description']\ 126 | .replace(':', '')\ 127 | .replace(',', '')\ 128 | , 129 | status=status, 130 | scheduled=scheduled, 131 | duration=ends_at, 132 | )) 133 | 134 | for child in tree['children']: 135 | print_tree(child, depth + 1) 136 | 137 | def tree_search(tree, uuid): 138 | if tree['node']['uuid'] == uuid: 139 | return tree['node'] 140 | for subtree in tree['children']: 141 | res = tree_search(subtree, uuid) 142 | if res: 143 | return res 144 | 145 | def tree_search_descr_prefix(tree, descr_prefix): 146 | if tree['node']['description'].startswith(descr_prefix): 147 | return tree['node'] 148 | for subtree in tree['children']: 149 | res = tree_search_descr_prefix(subtree, descr_prefix) 150 | if res: 151 | return res 152 | 153 | def tree_validate_duration(tree): 154 | has_childs = not not tree['children'] 155 | has_duration = not not tree['node']['duration'] 156 | if has_childs and has_duration: 157 | print("{uuid} ({description}) has subtasks, but also has duration set to {duration}, unset the duration!".format(**tree['node'])) 158 | if not has_childs and not has_duration: 159 | print("{uuid} ({description}) has no subtasks, and has duration unset, set the duration!".format(**tree['node'])) 160 | 161 | for subtree in tree['children']: 162 | tree_validate_duration(subtree) 163 | 164 | def tree_validate_status(tree): 165 | if not tree['children']: 166 | return 167 | 168 | can_have_pending_subtasks = tree['node']['status'] == 'pending' 169 | has_any_pending_subtasks = False 170 | 171 | for subtree in tree['children']: 172 | if subtree['node']['status'] == 'pending': 173 | has_any_pending_subtasks = True 174 | tree_validate_status(subtree) 175 | 176 | if has_any_pending_subtasks and not can_have_pending_subtasks: 177 | print("{uuid} ({description}) is not in pending status, but has pending subtasks".format(**tree['node'])) 178 | if can_have_pending_subtasks and not has_any_pending_subtasks: 179 | print("{uuid} ({description}) is in pending status, but has no pending subtasks".format(**tree['node'])) 180 | 181 | def tree_validate_scheduled(tree): 182 | if not tree['node']['scheduled']: 183 | print("{uuid} ({description}) is not scheduled!".format(**tree['node'])) 184 | 185 | last_subtask_ends_at = None 186 | last_subtask = None 187 | 188 | for subtask in tree['children']: 189 | if tree['node']['scheduled'] and subtask['node']['scheduled'] \ 190 | and tree['node']['scheduled'] < subtask['node']['scheduled']: 191 | print("{uuid} ({description}) is scheduled for {scheduled}, earlier than its subtask:".format(**tree['node'])) 192 | print(" {uuid} ({description}) {scheduled}".format(**subtask['node'])) 193 | 194 | tree_validate_scheduled(subtask) 195 | 196 | subtask_ends_at = subtask['node']['scheduled'] 197 | if subtask['node']['duration']: 198 | subtask_ends_at += timedelta(seconds=subtask['node']['duration']) 199 | if not last_subtask_ends_at or subtask_ends_at > last_subtask_ends_at: 200 | last_subtask_ends_at = subtask_ends_at 201 | last_subtask = subtask['node'] 202 | 203 | if last_subtask_ends_at and tree['node']['scheduled'] > last_subtask_ends_at: 204 | print("{uuid} ({description}) is scheduled for {scheduled}, later than its last subtask is completed:".format(**tree['node'])) 205 | print(" {uuid} ({description}) ends at {ends_at}".format(**subtask['node'], ends_at=last_subtask_ends_at)) 206 | 207 | 208 | def tree_validate(tree, codename): 209 | # Ensure no orphan subtasks 210 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 211 | cur.execute("EXECUTE SEL_ALL_BY_CODENAME_FROM_TREE_EDIT (%s)", (codename,)) 212 | rows = cur.fetchall() 213 | for row in rows: 214 | if tree_search(tree, row['uuid']) is None: 215 | print("{uuid} ({description}) has codename in descrption, but is not linked; parent is {parent}".format(**row)) 216 | 217 | tree_validate_duration(tree) 218 | tree_validate_scheduled(tree) 219 | tree_validate_status(tree) 220 | #tree_validate_dod(tree, codename) 221 | 222 | tree = dict() 223 | 224 | if TOP_CODENAME: 225 | top_task = get_top_by_codename(TOP_CODENAME) 226 | TOP_UUID = top_task['uuid'] 227 | else: 228 | top_task = get_by_uuid(TOP_UUID) 229 | # TODO extract TOP_CODENAME 230 | 231 | tree['node'] = top_task 232 | tree['children'] = get_children_recursively(TOP_UUID) 233 | 234 | print_tree(tree) 235 | #tree_validate(tree, TOP_CODENAME) 236 | -------------------------------------------------------------------------------- /bin/taskdb_tree_view: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Format example: 4 | # 5 | # First time, say, there's "project's top level task" existing with its uuid. 6 | # User starts with this in editor: 7 | # 8 | # * project's top level task UUID=blah 9 | # 10 | # After editing, user ends up with: 11 | # 12 | # * project's top level task UUID=blah 13 | # * subtask 1 DURATION=3600 14 | # * subtask 2 DURATION=600 15 | # 16 | # Program creates task entries for new subtasks, saves duration attr, sets top task's dependencies list to UUIDs of new subtasks. 17 | # Next time editor will present this to user: 18 | # 19 | # * project's top level task UUID=blah 20 | # * subtask 1 DURATION=3600 UUID=blah1 21 | # * subtask 2 DURATION=600 UUID=blah2 22 | # 23 | # And maybe it'll add that DURATION of top level task will be the sum of subtasks. 24 | # 25 | # Later, user can insert any new lines - program will create task entry for them. 26 | # Subtasks can be moved around i.e. reparented. Dependencies graph will get updated. 27 | # Deleted entries with existing UUIDs will be marked as "deleted" in DB. 28 | 29 | import os 30 | import sys 31 | import subprocess 32 | import psycopg2 33 | import psycopg2.extras 34 | from datetime import datetime, timedelta 35 | import pytz 36 | from pprint import pprint 37 | 38 | 39 | TOP_UUID = sys.argv[1] 40 | TOP_CODENAME = sys.argv[2] if len(sys.argv) > 2 else None 41 | 42 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 43 | conn_string = file.read() 44 | 45 | conn = psycopg2.connect(conn_string) 46 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 47 | cur.execute("PREPARE SEL_BY_UUID_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE uuid = $1 AND status in ('completed', 'pending')"); 48 | cur.execute("PREPARE SEL_TOP_BY_CODENAME_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE (description ~ ('^' || $1 || '.*[(]TOP[)]$')) AND parent IS NULL AND status in ('completed', 'pending')"); 49 | cur.execute("PREPARE SEL_ALL_BY_CODENAME_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE (description ~ ('^' || $1)) AND status in ('completed', 'pending')"); 50 | cur.execute(""" 51 | PREPARE SEL_BY_PARENT_FROM_TREE_EDIT AS 52 | SELECT * FROM tasks 53 | WHERE 54 | ( 55 | parent = $1 56 | OR 57 | ( 58 | uuid::text IN 59 | ( 60 | SELECT UNNEST(STRING_TO_ARRAY(dependencies, '\n')) 61 | FROM tasks 62 | WHERE uuid=$1 63 | ) 64 | ) 65 | ) 66 | AND 67 | status in ('completed', 'pending') 68 | ORDER BY scheduled 69 | """); 70 | # Pending only: 71 | #cur.execute("PREPARE SEL_BY_PARENT_FROM_TREE_EDIT AS SELECT * FROM tasks WHERE parent = $1 AND status = 'pending' ORDER BY scheduled"); 72 | 73 | def get_top_by_codename(codename): 74 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 75 | cur.execute("EXECUTE SEL_TOP_BY_CODENAME_FROM_TREE_EDIT (%s)", (codename,)) 76 | row = cur.fetchone() 77 | return row 78 | 79 | def get_by_uuid(uuid): 80 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 81 | cur.execute("EXECUTE SEL_BY_UUID_FROM_TREE_EDIT (%s)", (uuid,)) 82 | row = cur.fetchone() 83 | return row 84 | 85 | def get_by_parent(uuid): 86 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 87 | cur.execute("EXECUTE SEL_BY_PARENT_FROM_TREE_EDIT (%s)", (uuid,)) 88 | rows = cur.fetchall() 89 | return rows 90 | 91 | def get_children_recursively(uuid): 92 | children_db_rows = get_by_parent(uuid) 93 | children_list = [] 94 | for row in children_db_rows: 95 | child = dict() 96 | child['node'] = row 97 | child['children'] = get_children_recursively(row['uuid']) 98 | children_list.append(child) 99 | return children_list 100 | 101 | def status_str(row_status): 102 | if row_status == 'pending': 103 | return ' '; 104 | if row_status == 'completed': 105 | return 'V'; 106 | else: 107 | return 'Z'; 108 | 109 | def print_tree(tree, depth=0): 110 | row = tree['node'] 111 | if row['scheduled']: 112 | sched_str = row['scheduled'].strftime('%m-%d %H:%M') 113 | else: 114 | sched_str = '-' 115 | 116 | print( 117 | '%-60s' % ( 118 | '*' * (depth + 1) + ' ' + 119 | row['description'] 120 | ) + 121 | '[ %s]' % status_str(row['status']) + 122 | ' %s' % sched_str + 123 | ' %dh' % (int(row['duration'] or 0) // 3600) + 124 | '' 125 | ) 126 | 127 | for child in tree['children']: 128 | print_tree(child, depth + 1) 129 | 130 | def tree_search(tree, uuid): 131 | if tree['node']['uuid'] == uuid: 132 | return tree['node'] 133 | for subtree in tree['children']: 134 | res = tree_search(subtree, uuid) 135 | if res: 136 | return res 137 | 138 | def tree_search_descr_prefix(tree, descr_prefix): 139 | if tree['node']['description'].startswith(descr_prefix): 140 | return tree['node'] 141 | for subtree in tree['children']: 142 | res = tree_search_descr_prefix(subtree, descr_prefix) 143 | if res: 144 | return res 145 | 146 | def tree_validate_duration(tree): 147 | has_childs = not not tree['children'] 148 | has_duration = not not tree['node']['duration'] 149 | if has_childs and has_duration: 150 | print("{uuid} ({description}) has subtasks, but also has duration set to {duration}, unset the duration!".format(**tree['node'])) 151 | if not has_childs and not has_duration: 152 | print("{uuid} ({description}) has no subtasks, and has duration unset, set the duration!".format(**tree['node'])) 153 | 154 | for subtree in tree['children']: 155 | tree_validate_duration(subtree) 156 | 157 | def tree_validate_status(tree): 158 | if not tree['children']: 159 | return 160 | 161 | can_have_pending_subtasks = tree['node']['status'] == 'pending' 162 | has_any_pending_subtasks = False 163 | 164 | for subtree in tree['children']: 165 | if subtree['node']['status'] == 'pending': 166 | has_any_pending_subtasks = True 167 | tree_validate_status(subtree) 168 | 169 | if has_any_pending_subtasks and not can_have_pending_subtasks: 170 | print("{uuid} ({description}) is not in pending status, but has pending subtasks".format(**tree['node'])) 171 | if can_have_pending_subtasks and not has_any_pending_subtasks: 172 | print("{uuid} ({description}) is in pending status, but has no pending subtasks".format(**tree['node'])) 173 | 174 | def tree_validate_scheduled(tree): 175 | if not tree['node']['scheduled']: 176 | print("{uuid} ({description}) is not scheduled!".format(**tree['node'])) 177 | 178 | last_subtask_ends_at = None 179 | last_subtask = None 180 | 181 | for subtask in tree['children']: 182 | if tree['node']['scheduled'] < subtask['node']['scheduled']: 183 | print("{uuid} ({description}) is scheduled for {scheduled}, earlier than its subtask:".format(**tree['node'])) 184 | print(" {uuid} ({description}) {scheduled}".format(**subtask['node'])) 185 | 186 | if subtask['node']['duration']: 187 | subtask_duration = timedelta(seconds=subtask['node']['duration']) 188 | else: 189 | subtask_duration = timedelta(seconds=0) 190 | 191 | if tree['node']['scheduled'] < subtask['node']['scheduled'] + subtask_duration: 192 | print("{uuid} ({description}) is scheduled for {scheduled}, earlier than its subtask completes:".format(**tree['node'])) 193 | print(" {uuid} ({description}) {scheduled}".format(**subtask['node'])) 194 | 195 | tree_validate_scheduled(subtask) 196 | 197 | subtask_ends_at = subtask['node']['scheduled'] 198 | if subtask['node']['duration']: 199 | subtask_ends_at += timedelta(seconds=subtask['node']['duration']) 200 | if not last_subtask_ends_at or subtask_ends_at > last_subtask_ends_at: 201 | last_subtask_ends_at = subtask_ends_at 202 | last_subtask = subtask['node'] 203 | 204 | if last_subtask_ends_at and tree['node']['scheduled'] > last_subtask_ends_at: 205 | print("{uuid} ({description}) is scheduled for {scheduled}, later than its last subtask is completed:".format(**tree['node'])) 206 | print(" {uuid} ({description}) ends at {ends_at}".format(**subtask['node'], ends_at=last_subtask_ends_at)) 207 | 208 | def tree_validate_dod(tree, codename): 209 | """ 210 | Check that task tree includes all sensible items from Definition of Done. 211 | """ 212 | dod_items = [ 213 | # Analysis 214 | "Analyze the task", 215 | "Investigate the problem", 216 | "Report the analysis", 217 | 218 | # Design 219 | "Design the solution", 220 | "Get approval on the solution design", 221 | 222 | # Planning 223 | "Find a reviewer", 224 | "Estimate", 225 | "Schedule", 226 | "Book QA resources", 227 | "Report the schedule", 228 | 229 | # Implementation 230 | "Add tests", 231 | "Implement the solution", 232 | "Refactor", 233 | "Update doc", 234 | "Update changelog", 235 | "Backport?", 236 | "Pass QA", 237 | 238 | # Review 239 | "Pass review", 240 | "Submit for review", 241 | "Address review feedback", 242 | "Pass QA after review", 243 | 244 | # Integration 245 | "Merge", 246 | 247 | # Post-release 248 | "Feature released?", 249 | "Update customer tickets?", 250 | ] 251 | for item in dod_items: 252 | item_type = "Optional " if item[-1] == '?' else "Mandatory" 253 | if not tree_search_descr_prefix(tree, f"{codename} {item}"): 254 | print(f"{item_type} item missing: {item}") 255 | else: 256 | print(f"{item_type} item present: {item}") 257 | 258 | 259 | 260 | def tree_validate(tree, codename): 261 | # Ensure no orphan subtasks 262 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 263 | cur.execute("EXECUTE SEL_ALL_BY_CODENAME_FROM_TREE_EDIT (%s)", (codename,)) 264 | rows = cur.fetchall() 265 | for row in rows: 266 | if tree_search(tree, row['uuid']) is None: 267 | print("{uuid} ({description}) has codename in descrption, but is not linked; parent is {parent}".format(**row)) 268 | 269 | tree_validate_duration(tree) 270 | tree_validate_scheduled(tree) 271 | tree_validate_status(tree) 272 | #tree_validate_dod(tree, codename) 273 | 274 | tree = dict() 275 | 276 | if TOP_CODENAME: 277 | top_task = get_top_by_codename(TOP_CODENAME) 278 | TOP_UUID = top_task['uuid'] 279 | else: 280 | top_task = get_by_uuid(TOP_UUID) 281 | # TODO extract TOP_CODENAME 282 | 283 | tree['node'] = top_task 284 | tree['children'] = get_children_recursively(TOP_UUID) 285 | 286 | print_tree(tree) 287 | tree_validate(tree, TOP_CODENAME) 288 | -------------------------------------------------------------------------------- /bin/tdone: -------------------------------------------------------------------------------- 1 | # Mark task as completed 2 | # TODO Accept UUID or short number of record from last query from same terminal 3 | 4 | UUID="$1" 5 | 6 | if [[ -z "$UUID" ]]; then 7 | UUID=$(cat ~/.taskdb/last_uuid) 8 | fi 9 | # TODO must use prepared statements 10 | echo "UPDATE tasks SET status = 'completed' WHERE uuid = '$UUID';" | psql 11 | -------------------------------------------------------------------------------- /bin/tdt: -------------------------------------------------------------------------------- 1 | # Show pending tasks scheduled or due earlier than tomorrow 2 | MYPATH=$(dirname $0) 3 | "$MYPATH"/taskdb_report tdt 4 | -------------------------------------------------------------------------------- /bin/tdtnw: -------------------------------------------------------------------------------- 1 | # Show pending work-related tasks scheduled or due earlier than tomorrow 2 | MYPATH=$(dirname $0) 3 | "$MYPATH"/taskdb_report tdtnw 4 | -------------------------------------------------------------------------------- /bin/tdtw: -------------------------------------------------------------------------------- 1 | # Show pending work-related tasks scheduled or due earlier than tomorrow 2 | MYPATH=$(dirname $0) 3 | "$MYPATH"/taskdb_report tdtw 4 | -------------------------------------------------------------------------------- /bin/tea: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Edit task annotation""" 4 | 5 | import os 6 | import pathlib 7 | import sys 8 | import subprocess 9 | import psycopg2 10 | import psycopg2.extras 11 | 12 | if len(sys.argv) >= 2: 13 | uuid = sys.argv[1] 14 | else: 15 | LAST_UUID_FILE_PATH = str(pathlib.Path.home()) + '/.taskdb/last_uuid' 16 | with open(LAST_UUID_FILE_PATH, 'r') as f: 17 | uuid = f.read() 18 | uuid = uuid.strip() 19 | 20 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 21 | conn_string = file.read() 22 | 23 | conn = psycopg2.connect(conn_string) 24 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 25 | cur.execute("PREPARE SEL_FROM_TEA AS SELECT annotation FROM tasks WHERE uuid = $1"); 26 | cur.execute("PREPARE UPD_FROM_TEA AS UPDATE tasks SET annotation = $1 WHERE uuid = $2"); 27 | 28 | cur.execute("EXECUTE SEL_FROM_TEA (%s)", (uuid,)) 29 | row = cur.fetchone() 30 | if not row: 31 | print('Failed to query the task.') 32 | sys.exit(1) 33 | 34 | # Put content into file 35 | if row['annotation']: 36 | with open(uuid, 'wb') as f: 37 | f.write(row['annotation'].encode('utf-8')) 38 | 39 | # Oddly, doesn't work when pointed at a script 40 | # FIXME Call $EDITOR 41 | EDITOR = os.environ.get('EDITOR','vim') 42 | subprocess.call([EDITOR, str(uuid)]) 43 | 44 | with open(uuid, 'rb') as f: 45 | annotation_edited = f.read().decode('utf-8') 46 | os.unlink(uuid) 47 | 48 | if annotation_edited == row['annotation']: 49 | print('No edits detected.') 50 | sys.exit(0) 51 | 52 | cur.execute("EXECUTE UPD_FROM_TEA (%s, %s)", (annotation_edited, uuid)) 53 | 54 | conn.commit() 55 | -------------------------------------------------------------------------------- /bin/tlog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Create a new task in 'completed' state""" 4 | 5 | import os 6 | import sys 7 | import psycopg2 8 | import psycopg2.extras 9 | 10 | description = sys.argv[1] 11 | if len(sys.argv) > 2: 12 | project = sys.argv[2] 13 | else: 14 | project = None 15 | 16 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 17 | conn_string = file.read() 18 | 19 | conn = psycopg2.connect(conn_string) 20 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 21 | cur.execute("PREPARE INS_FROM_TLOG AS INSERT INTO tasks (scheduled, status, description, project) VALUES(CURRENT_TIMESTAMP, 'completed', $1, $2) RETURNING uuid"); 22 | 23 | cur.execute("EXECUTE INS_FROM_TLOG (%s, %s)", (description, project)) 24 | conn.commit() 25 | row = cur.fetchone() 26 | print(row['uuid']) 27 | -------------------------------------------------------------------------------- /doc/demo/configs/psqlrc: -------------------------------------------------------------------------------- 1 | \pset pager always 2 | \pset format wrapped 3 | \x on 4 | -------------------------------------------------------------------------------- /doc/demo/configs/taskdb_cal_sync_setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -a /HOME/{.password-store,.gnupg,.vdirsyncer} ~ 4 | export LC_ALL=C.UTF-8 LANG=C.UTF-8 # vdirsyncer needs this 5 | yes | vdirsyncer discover fastmail_calendar 2> >(tee discover.out) 6 | CAL_UUID=$(grep '"taskdb-demo"' discover.out | cut -d '"' -f 2) 7 | mkdir -p ~/.taskdb 8 | ln -sv ~/.calendars/"$CAL_UUID" ~/.taskdb/calendar 9 | 10 | echo "Next step: launch taskdb_cal_sync_loop" 11 | -------------------------------------------------------------------------------- /doc/demo/configs/vdirsyncer: -------------------------------------------------------------------------------- 1 | # Put this into ~/.vdirsyncer/config 2 | 3 | [general] 4 | # A folder where vdirsyncer can store some metadata about each pair. 5 | status_path = "~/.vdirsyncer/status/" 6 | 7 | # CALDAV 8 | [pair fastmail_calendar] 9 | a = "fastmail_calendar_local" 10 | b = "fastmail_calendar_remote" 11 | collections = ["from a", "from b"] 12 | 13 | # Calendars also have a color property 14 | metadata = ["displayname", "color"] 15 | conflict_resolution = ["command", "/usr/bin/taskdb_cal_sync_conflict_resolve"] 16 | 17 | [storage fastmail_calendar_local] 18 | type = "filesystem" 19 | path = "~/.calendars/" 20 | fileext = ".ics" 21 | 22 | [storage fastmail_calendar_remote] 23 | type = "caldav" 24 | url = "https://caldav.messagingengine.com/" 25 | username = "andrey_utkin@fastmail.com" 26 | password.fetch = ["command", "pass", "taskdb/caldav"] 27 | -------------------------------------------------------------------------------- /doc/demo/datasets/fictional_task.psql_copy: -------------------------------------------------------------------------------- 1 | 2020-05-05 13:00:00+01 fictional-task: Requirement gathering and analysis [ft-analyze] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:05:17+00 2020-03-27 23:28:44.675107+00 \N pending 3a3ec509-b997-4c0a-9408-a295b7c2541e ft-analyze 2 | 2020-05-05 19:00:00+01 fictional-task: Design [ft-design] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:07:03+00 2020-03-27 23:28:44.675107+00 \N pending 1fe686d9-4f83-4dd9-9eca-f56ae81931eb ft-design 3 | 2020-05-06 18:00:00+01 fictional-task: Implementation [ft-impl] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:07:30+00 2020-03-27 23:28:44.675107+00 \N pending 4a5c79f0-1f72-4bb1-a8ee-5d0b726b14f8 ft-impl 4 | 2020-05-08 12:00:00+01 fictional-task: Deployment [ft-deploy] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:08:37+00 2020-03-27 23:28:44.675107+00 \N pending 769ab80c-6a60-4d49-b57e-5578303bdbe0 ft-deploy 5 | 2020-05-04 11:00:00+01 ft-analyze: arrange a decision making meeting with stakeholders \N \N \N \N 3600 \N 3a3ec509-b997-4c0a-9408-a295b7c2541e \N 2020-03-27 23:11:36+00 2020-03-27 23:28:44.675107+00 \N pending 19a2df44-caeb-4ffd-b0a7-37388e4ac0bf \N 6 | 2020-05-05 12:00:00+01 ft-analyze: decision making meeting with stakeholders \N \N \N \N 3600 \N 3a3ec509-b997-4c0a-9408-a295b7c2541e \N 2020-03-27 23:12:25+00 2020-03-27 23:28:44.675107+00 \N pending 2906420d-2d64-4d24-8a91-baa11e6a6ecc \N 7 | 2020-05-05 15:00:00+01 ft-design: identify approaches \N \N \N \N 3600 \N 1fe686d9-4f83-4dd9-9eca-f56ae81931eb \N 2020-03-27 23:13:35+00 2020-03-27 23:28:44.675107+00 \N pending 42a5fcb7-2b12-4c98-9263-a74f06e1ebd9 \N 8 | 2020-05-04 10:00:00+01 ft-analyze: read feature request \N \N \N \N 3600 \N 3a3ec509-b997-4c0a-9408-a295b7c2541e \N 2020-03-27 23:10:11+00 2020-03-27 23:28:44.675107+00 \N pending e8d3f632-c36b-4745-97fc-6dad5b89a894 \N 9 | 2020-05-05 16:00:00+01 ft-design: evaluate approaches \N \N \N \N 7200 \N 1fe686d9-4f83-4dd9-9eca-f56ae81931eb \N 2020-03-27 23:14:19+00 2020-03-27 23:28:44.675107+00 \N pending 57dfca7b-3429-4162-839b-adb22b8a45c5 \N 10 | 2020-05-05 18:00:00+01 ft-design: identify optimal approach \N \N \N \N 3600 \N 1fe686d9-4f83-4dd9-9eca-f56ae81931eb \N 2020-03-27 23:14:43+00 2020-03-27 23:28:44.675107+00 \N pending b64039d5-640f-4cde-97c2-f8931b44566d \N 11 | 2020-05-06 10:00:00+01 ft-impl-backend: add tests \N \N \N \N 3600 \N 19660f36-b189-4db2-8b31-547129ac763c \N 2020-03-27 23:16:21+00 2020-03-27 23:28:44.675107+00 \N pending 151b6f39-a9ec-4bc3-98c3-865e040454f1 \N 12 | 2020-05-06 14:00:00+01 ft-impl: Implement backend [ft-impl-backend] \N \N \N \N 0 \N 4a5c79f0-1f72-4bb1-a8ee-5d0b726b14f8 \N 2020-03-27 23:18:05+00 2020-03-27 23:28:44.675107+00 \N pending 19660f36-b189-4db2-8b31-547129ac763c ft-impl-backend 13 | 2020-05-06 11:00:00+01 ft-impl-backend: implement functionality to pass tests \N \N \N \N 10800 \N 19660f36-b189-4db2-8b31-547129ac763c \N 2020-03-27 23:17:40+00 2020-03-27 23:28:44.675107+00 \N pending f6fd5a13-b2ac-48e2-8f8b-7eedd0c20f7f \N 14 | 2020-05-06 16:00:00+01 ft-impl-frontend: implement functionality to pass tests \N \N \N \N 7200 \N ba08edfc-b981-49a1-a04d-71d719a71868 \N 2020-03-27 23:19:41+00 2020-03-27 23:29:50+00 \N pending 05e9d3b4-0cf9-42e8-aacf-a1f99753a78a \N 15 | 2020-05-07 11:00:00+01 ft-test: fix test failures (placeholder) \N \N \N \N 10800 \N 1ed55ce4-43d3-4c95-a68d-82ce29fc5dbb \N 2020-03-27 23:22:10+00 2020-03-27 23:39:29.009144+00 \N pending 294e35b7-ca98-46b0-8181-74d5c38be8d9 \N 16 | 2020-05-07 15:00:00+01 ft-deploy: merge to master, when CI clean \N \N \N \N 3600 \N 769ab80c-6a60-4d49-b57e-5578303bdbe0 \N 2020-03-27 23:23:31+00 2020-03-27 23:28:44.675107+00 \N pending 2d790035-cce3-40c1-b06d-2c693cf76935 \N 17 | 2020-05-08 11:00:00+01 ft-deploy: make release, when CI clean \N \N \N \N 3600 \N 769ab80c-6a60-4d49-b57e-5578303bdbe0 \N 2020-03-27 23:25:23+00 2020-03-27 23:28:44.675107+00 \N pending 5a80f4ed-0d5e-414d-9927-8f5d8b231853 \N 18 | 2020-05-08 10:00:00+01 ft-deploy: analyze test results from CI job of master branch \N \N \N \N 3600 \N 769ab80c-6a60-4d49-b57e-5578303bdbe0 \N 2020-03-27 23:24:59+00 2020-03-27 23:28:44.675107+00 \N pending 6d505542-8c5e-4d11-bcd7-51391b010f55 \N 19 | 2020-05-08 15:00:00+01 ft-maint: code cleanup \N \N \N \N 7200 \N 2296336b-0c99-42e5-b5c4-c550404f6aa7 \N 2020-03-27 23:26:50+00 2020-03-27 23:28:44.675107+00 \N pending df971cfd-08c8-4813-b5d5-faaa26a4d14c \N 20 | 2020-05-08 17:00:00+01 fictional-task: Maintenance [ft-maint] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:08:58+00 2020-03-27 23:28:44.675107+00 \N pending 2296336b-0c99-42e5-b5c4-c550404f6aa7 ft-maint 21 | 2020-05-07 14:00:00+01 fictional-task: Testing [ft-test] \N \N \N \N 0 \N c76424ef-bf8f-495c-89f7-e75adb595a32 \N 2020-03-27 23:08:09+00 2020-03-27 23:28:44.675107+00 \N pending 1ed55ce4-43d3-4c95-a68d-82ce29fc5dbb ft-test 22 | 2020-05-06 18:00:00+01 ft-test: push to branch and trigger CI job \N \N \N \N 3600 \N 1ed55ce4-43d3-4c95-a68d-82ce29fc5dbb \N 2020-03-27 23:21:00+00 2020-03-27 23:28:44.675107+00 \N pending 9b064941-9fdf-4e35-9572-47eb4a7280ea \N 23 | 2020-05-07 10:00:00+01 ft-test: analyze test results from CI job \N \N \N \N 3600 \N 1ed55ce4-43d3-4c95-a68d-82ce29fc5dbb \N 2020-03-27 23:21:47+00 2020-03-27 23:28:44.675107+00 \N pending 15b7a1b2-a422-4e8a-ba8c-e529886b4edd \N 24 | 2020-05-06 18:00:00+01 ft-impl: Implement frontend [ft-impl-frontend] \N \N \N \N 0 \N 4a5c79f0-1f72-4bb1-a8ee-5d0b726b14f8 \N 2020-03-27 23:18:39+00 2020-03-27 23:31:04.645865+00 \N pending ba08edfc-b981-49a1-a04d-71d719a71868 ft-impl-frontend 25 | 2020-05-06 15:00:00+01 ft-impl-frontend: add tests \N \N \N \N 3600 \N ba08edfc-b981-49a1-a04d-71d719a71868 \N 2020-03-27 23:18:49+00 2020-03-27 23:31:11.010876+00 \N pending f1252574-f1d6-4e64-8572-cbdcf0971864 \N 26 | 2020-05-08 19:00:00+01 Implement a certain feature [fictional-task] \N \N \N \N 0 \N \N \N 2020-03-27 23:04:32+00 2020-03-27 23:28:44.675107+00 \N pending c76424ef-bf8f-495c-89f7-e75adb595a32 fictional-task 27 | -------------------------------------------------------------------------------- /doc/demo/datasets/sleep.psql_copy: -------------------------------------------------------------------------------- 1 | 2019-12-19 01:15:00+00 Sleep \N \N \N \N 31500 \N \N \N 2019-12-19 00:50:55+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed 5d33223b-ef6d-4b7e-87f2-0cb27397b798 \N 2 | 2020-01-05 00:00:00+00 sleep \N \N \N \N 28800 \N \N \N 2019-12-29 20:15:27+00 2020-03-06 23:51:14.148373+00 2020-01-06 18:21:37.463502+00 completed 4667aece-4725-424b-ad45-1be96e77439a \N 3 | 2020-01-03 01:15:00+00 sleep \N \N \N \N 23400 \N \N \N 2019-12-29 18:41:59+00 2020-03-06 23:51:14.148373+00 2020-01-05 09:34:29.076839+00 completed 181d11f1-a7f0-49c4-b8ef-1f17718198ba \N 4 | 2020-01-27 00:00:00+00 sleep \N \N \N \N 34200 \N \N \N 2020-01-19 23:25:27+00 2020-03-06 23:51:14.148373+00 2020-01-30 00:27:01.527901+00 completed 4832e443-a1ca-440e-8ebd-91502e504cf8 \N 5 | 2019-12-20 01:30:00+00 Sleep \N \N \N \N 28800 \N \N \N 2019-12-20 10:16:51+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed b9abb4c6-8a80-4773-96dc-704092a33bb2 \N 6 | 2020-02-09 23:45:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-11 12:50:45.004385+00 completed a2c89da7-cda7-4f2f-a996-cb44b469288a \N 7 | 2020-02-14 23:45:00+00 sleep \N \N \N \N 27900 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-16 13:55:47.61203+00 completed 0b13cd65-e7cc-43c2-8605-c6abc48d2d3a \N 8 | 2020-01-29 00:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-01-30 00:28:07.560116+00 completed 951a6639-02d5-4561-b14f-581b73329a28 \N 9 | 2020-01-11 01:00:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-01-05 08:37:53.334663+00 2020-03-06 23:51:14.148373+00 2020-01-12 22:56:56.18532+00 completed 0dc1f47d-06b1-4483-b704-550284a91f70 \N 10 | 2019-12-21 00:00:00+00 Sleep \N \N \N \N 34200 \N \N \N 2019-12-20 23:32:02+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed 26d19f11-d088-42ba-b9be-5b5b76f3c06f \N 11 | 2020-01-12 02:00:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-01-05 08:37:55.468607+00 2020-03-06 23:51:14.148373+00 2020-01-13 12:22:33.201525+00 completed fe3aedb1-47ec-4411-a73c-e770390f5640 \N 12 | 2020-01-13 00:15:00+00 sleep \N \N \N \N 31500 \N \N \N 2020-01-05 08:37:57.287001+00 2020-03-06 23:51:14.148373+00 2020-01-16 11:22:02.343509+00 completed 97b12977-61c6-4a6f-8417-a027ba622e13 \N 13 | 2020-01-24 23:30:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-01-19 09:15:55.603721+00 2020-03-06 23:51:14.148373+00 2020-01-25 22:54:02.437339+00 completed 27935562-7679-445f-a8f1-9319a0f9e7a5 \N 14 | 2020-01-23 00:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-19 09:15:50.852277+00 2020-03-06 23:51:14.148373+00 2020-01-25 22:53:44.297438+00 completed 2a2d2807-06a1-4970-bdc9-fd4f467aa213 \N 15 | 2020-02-15 23:30:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-16 13:55:47.61203+00 completed 91e6e957-adb5-45cf-b9e7-65553284d9b7 \N 16 | 2020-03-04 00:30:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-05 16:51:42.899495+00 2020-03-05 16:51:42.899495+00 completed b855692c-f97f-489e-9b18-68ff601e93ca \N 17 | 2019-12-13 23:30:00+00 Sleep \N \N \N \N 39600 \N \N \N 2019-12-13 23:28:00+00 2020-03-06 23:51:14.148373+00 2019-12-16 18:35:49.797657+00 completed 9a23f931-1456-4198-819d-72828a04da9c \N 18 | 2019-12-15 01:00:00+00 Sleep \N \N \N \N 32400 \N \N \N 2019-12-15 00:57:33+00 2020-03-06 23:51:14.148373+00 2019-12-16 18:35:49.797657+00 completed d5db03fd-17fa-4890-b2a8-6a74dcb6fac1 \N 19 | 2019-12-24 02:30:00+00 Sleep \N \N \N \N 27000 \N \N \N 2019-12-24 00:37:49+00 2020-03-06 23:51:14.148373+00 2019-12-26 23:12:36.708241+00 completed c9838ea7-2017-4059-832f-f81f0149c294 \N 20 | 2019-12-16 01:45:00+00 Sleep \N \N \N \N 24300 \N \N \N 2019-12-16 14:07:12+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed ef218c98-c6a8-4584-9284-905dad3f153d \N 21 | 2019-12-16 23:15:00+00 Sleep \N \N \N \N 33300 \N \N \N 2019-12-17 08:37:11+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed 8283cce8-9b92-41bb-9a99-087c6c3b0680 \N 22 | 2019-12-18 00:45:00+00 Sleep \N \N \N \N 29700 \N \N \N 2019-12-18 10:23:18+00 2020-03-06 23:51:14.148373+00 2019-12-22 17:28:47.209614+00 completed 8e9947ee-37e3-44c9-b717-1a4a030857ed \N 23 | 2020-03-14 05:30:00+00 sleep \N \N \N \N 18000 \N \N \N 2020-03-14 11:59:51+00 2020-03-15 23:54:27.088384+00 2020-03-15 23:54:27.088384+00 completed c2d27b7a-bf8f-4050-aada-e1cfc6184289 \N 24 | 2020-03-30 00:15:00+01 Sleep \N \N \N \N 33300 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-30 19:13:52.745624+01 2020-03-30 19:13:52.745624+01 completed 67f055f9-dc5f-4125-939d-7f1bf118e9a5 \N 25 | 2020-03-25 00:30:00+00 Sleep \N \N \N \N 27900 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-26 09:02:54.72137+00 2020-03-26 09:02:54.72137+00 completed 88b92eff-7bd8-4e45-8a51-6ab11ca7bf73 \N 26 | 2020-03-26 23:30:00+00 Sleep \N \N \N \N 30600 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-27 09:54:20.707649+00 2020-03-27 09:54:20.707649+00 completed 8b4a4d65-950a-46cd-9375-6c70e03efcff \N 27 | 2020-03-23 22:45:00+00 Sleep \N \N \N \N 35100 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-24 10:40:23.490838+00 2020-03-24 10:40:23.490838+00 completed 79670a59-1a6f-4d8e-970e-97aae0ec0cc2 \N 28 | 2020-03-15 17:00:00+00 Sleep \N \N \N \N 7200 \N \N \N 2020-03-15 19:46:44+00 2020-03-16 00:59:50.59645+00 2020-03-16 00:59:50.59645+00 completed df118fd0-d462-46ab-a063-668f818bf376 \N 29 | 2020-04-01 00:15:00+01 Sleep \N \N \N \N 31500 \N \N \N 2020-03-30 22:21:42.986711+01 2020-04-02 11:52:40.374737+01 2020-04-02 11:52:40.374737+01 completed 62d9ce10-caee-4535-8232-9dd244a928e8 \N 30 | 2020-03-31 02:30:00+01 Sleep \N \N \N \N 20700 \N \N \N 2020-03-30 22:22:00+01 2020-04-01 10:03:38.627717+01 2020-04-01 10:03:38.627717+01 completed 32e6700d-67a8-4b75-8062-16e82235947e \N 31 | 2020-04-01 23:15:00+01 Sleep \N \N \N \N 37800 \N \N \N 2020-03-30 22:28:17.627003+01 2020-04-02 11:52:40.374737+01 2020-04-02 11:52:40.374737+01 completed fcbdf805-5b03-4929-ab48-3493c1a8f57d \N 32 | 2020-03-19 23:45:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-20 08:49:57.529003+00 2020-03-20 08:49:57.529003+00 completed e2d120f5-ff8b-4adf-bcb1-54a6e6e9ca0d \N 33 | 2020-03-26 00:30:00+00 Sleep \N \N \N \N 27900 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-27 09:54:20.707649+00 2020-03-27 09:54:20.707649+00 completed f4aaea73-b9a4-4823-9d27-c8d27973ee5e \N 34 | 2020-01-30 23:15:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed 2a2c9c45-eaa1-4d5c-bc16-98bc5880e3f3 \N 35 | 2019-12-25 00:00:00+00 sleep \N \N \N \N 36000 \N \N \N 2019-12-24 23:41:30+00 2020-03-06 23:51:14.148373+00 2019-12-26 23:12:36.708241+00 completed b43ae7ea-991b-4d08-9dec-bac4aef90787 \N 36 | 2020-02-02 23:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed 93b374ea-137f-4a12-8877-1fa0bb351359 \N 37 | 2019-12-26 02:00:00+00 Sleep \N \N \N \N 30600 \N \N \N 2019-12-26 02:09:11+00 2020-03-06 23:51:14.148373+00 2019-12-27 21:20:05.237958+00 completed 1713dd2b-0401-4ff8-9d1d-4d921ba00d15 \N 38 | 2019-12-29 00:15:00+00 sleep \N \N \N \N 38700 \N \N \N 2019-12-26 22:43:29+00 2020-03-06 23:51:14.148373+00 2019-12-30 00:40:07.032147+00 completed 1f839602-c49b-4924-bb49-ea24073d8eca \N 39 | 2019-12-26 23:30:00+00 sleep \N \N \N \N 38700 \N \N \N 2019-12-26 22:35:21+00 2020-03-06 23:51:14.148373+00 2019-12-27 21:20:05.237958+00 completed e4f4e618-4b72-44c4-8ea6-5c07eab591fe \N 40 | 2019-12-28 00:45:00+00 sleep \N \N \N \N 27900 \N \N \N 2019-12-26 22:43:27+00 2020-03-06 23:51:14.148373+00 2019-12-29 23:25:22.735587+00 completed c03e3185-b73a-4aa6-860a-1a059ab4aba8 \N 41 | 2019-12-30 00:45:00+00 sleep \N \N \N \N 31500 \N \N \N 2019-12-26 22:43:54+00 2020-03-06 23:51:14.148373+00 2019-12-31 20:40:51.235225+00 completed 9ba62d4f-bb47-4efa-a669-2e385131e8fa \N 42 | 2019-12-22 02:30:00+00 Sleep \N \N \N \N 21600 \N \N \N 2019-12-22 02:18:08+00 2020-03-06 23:51:14.148373+00 2019-12-26 23:12:36.708241+00 completed c1e13bfa-d637-44b0-87fe-369984bd5b01 \N 43 | 2019-12-23 00:30:00+00 sleep \N \N \N \N 34200 \N \N \N 2019-12-23 12:14:12+00 2020-03-06 23:51:14.148373+00 2019-12-26 23:12:36.708241+00 completed 4c6b514a-c119-4925-8949-e14f5d29cd17 \N 44 | 2020-01-01 01:15:00+00 sleep \N \N \N \N 29700 \N \N \N 2019-12-29 17:50:17+00 2020-03-06 23:51:14.148373+00 2020-01-02 18:08:19.729561+00 completed cfd2f1dc-c62f-4e2f-b443-cdb0915cc4be \N 45 | 2019-12-31 00:45:00+00 sleep \N \N \N \N 30600 \N \N \N 2019-12-29 17:32:55+00 2020-03-06 23:51:14.148373+00 2020-01-02 18:08:19.729561+00 completed 1c7984dc-8002-4713-bbcc-e77c78319694 \N 46 | 2020-01-06 00:15:00+00 sleep \N \N \N \N 26100 \N \N \N 2019-12-29 20:15:58+00 2020-03-06 23:51:14.148373+00 2020-01-07 10:53:13.651617+00 completed 6b698d5d-2b9c-49af-be08-cf04dda34644 \N 47 | 2020-01-27 23:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-01-30 00:27:55.27331+00 completed 1216d1e5-c8ee-40dc-8074-18e9f733063b \N 48 | 2020-02-04 23:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed 9e550034-1a68-4b2a-b314-43cf74ab7a50 \N 49 | 2020-02-06 23:30:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-08 22:05:54.655501+00 completed 6e6a8498-02fe-40c9-be2a-fb237fc14221 \N 50 | 2020-02-08 00:00:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-09 13:13:28.842954+00 completed a6d8ed7a-780b-4ac0-926e-bbbc5ba8119e \N 51 | 2020-02-18 00:15:00+00 sleep \N \N \N \N 26100 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-21 11:14:58.529902+00 completed bfe3d956-8a01-41e0-8234-4a89290935d5 \N 52 | 2020-01-02 00:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2019-12-29 18:42:56+00 2020-03-06 23:51:14.148373+00 2020-01-03 10:33:45.700282+00 completed b4d512dd-0cfb-416b-a25d-083a18c4c8bf \N 53 | 2020-02-12 01:00:00+00 sleep \N \N \N \N 23400 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-14 19:14:47.636024+00 completed ba0125f8-47bc-4d34-a076-36cbffa4f9b3 \N 54 | 2020-02-21 00:30:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-23 21:05:09.572505+00 completed 34d5e491-184c-49c4-8f5b-38e03be84b25 \N 55 | 2020-02-22 00:45:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-23 21:05:09.572505+00 completed 065d9bb7-ffcb-49e4-89eb-a8a3060b037b \N 56 | 2020-02-23 00:30:00+00 sleep \N \N \N \N 36000 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-24 17:46:05.776816+00 completed 69e4e13d-c40d-4325-a102-2af4e51e054d \N 57 | 2020-02-24 00:15:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-01 23:04:34.452674+00 2020-02-25 18:50:43.984546+00 completed cb16df23-5585-47d5-a28c-aaf32a6730c4 \N 58 | 2020-01-03 22:30:00+00 sleep \N \N \N \N 36900 \N \N \N 2019-12-29 20:10:01+00 2020-03-06 23:51:14.148373+00 2020-01-05 09:34:29.076839+00 completed f7781586-671e-45b0-8860-356984ebda3d \N 59 | 2020-01-07 00:00:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-01-05 08:36:13.027107+00 2020-03-06 23:51:14.148373+00 2020-01-08 11:33:25.091859+00 completed 1e4dee2a-f0e0-4d4f-82ec-162884eb2b89 \N 60 | 2020-01-08 23:00:00+00 sleep \N \N \N \N 36900 \N \N \N 2020-01-05 08:37:47.082379+00 2020-03-06 23:51:14.148373+00 2020-01-09 15:02:09.877765+00 completed 1cb5800e-70e2-4d69-bb1b-70c0319400f1 \N 61 | 2020-01-08 02:00:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-01-05 08:37:44.401798+00 2020-03-06 23:51:14.148373+00 2020-01-09 15:02:09.877765+00 completed 8b3f7e92-f3f8-40c2-8a9c-b27e5c948e99 \N 62 | 2020-01-10 00:15:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-01-05 08:37:50.911345+00 2020-03-06 23:51:14.148373+00 2020-01-11 16:11:43.817248+00 completed 452bdf08-56c8-487f-98a8-d4766c70c21c \N 63 | 2020-02-13 00:30:00+00 sleep \N \N \N \N 25200 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-14 19:14:47.636024+00 completed a54c6c07-a446-4a8e-89ed-c5b13769e460 \N 64 | 2020-01-23 23:15:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-01-19 09:15:53.248733+00 2020-03-06 23:51:14.148373+00 2020-01-25 22:54:02.437339+00 completed fb3b100d-7d60-4ad0-99a8-ccc065aab2e5 \N 65 | 2020-01-21 23:45:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-19 09:15:59.5032+00 2020-03-06 23:51:14.148373+00 2020-01-23 12:49:30.025315+00 completed 6b0479e4-f6fe-4084-8dcb-04a1e450280e \N 66 | 2020-01-16 23:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-12 19:12:36.779437+00 2020-03-06 23:51:14.148373+00 2020-01-17 18:57:26.801554+00 completed 5c2d0e67-4550-4d4b-b129-fe58bd5a5ccd \N 67 | 2020-01-19 00:30:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-01-12 19:12:39.726508+00 2020-03-06 23:51:14.148373+00 2020-01-20 12:24:46.179923+00 completed 5e8764d0-f5b0-43bb-9d03-be7725836ba8 \N 68 | 2020-01-19 23:30:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-01-12 19:12:41.35344+00 2020-03-06 23:51:14.148373+00 2020-01-20 12:24:46.179923+00 completed 9e9456b4-7797-4848-9801-5e8243f186ab \N 69 | 2020-01-20 23:00:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-12 19:13:01.609526+00 2020-03-06 23:51:14.148373+00 2020-01-21 13:01:50.405954+00 completed 1ebb7f9e-629a-4075-8f45-e1f1596ca8ab \N 70 | 2020-01-13 23:00:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-12 19:12:26.264507+00 2020-03-06 23:51:14.148373+00 2020-01-16 11:22:02.343509+00 completed aab29e67-6b40-4de5-9dfa-0cf17df83ba4 \N 71 | 2020-01-14 22:45:00+00 sleep \N \N \N \N 37800 \N \N \N 2020-01-12 19:12:33.49297+00 2020-03-06 23:51:14.148373+00 2020-01-16 11:22:02.343509+00 completed 0e8706f9-df67-466c-a15c-e8d5a6602f75 \N 72 | 2020-01-17 23:00:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-01-12 19:12:38.188299+00 2020-03-06 23:51:14.148373+00 2020-01-19 10:21:08.420621+00 completed b2dbdb14-e2e4-49c1-8083-1dabfc44a1ca \N 73 | 2020-02-03 23:15:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed d40777ad-ed07-4a77-9e00-2782f1f244dc \N 74 | 2020-01-26 01:30:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-01-19 09:15:57.239277+00 2020-03-06 23:51:14.148373+00 2020-01-27 17:34:51.881837+00 completed 09ed20b3-0b45-4bb8-b575-0f27d32b45f8 \N 75 | 2020-02-14 00:45:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-16 13:55:47.61203+00 completed 2701cda6-a276-4d94-bc4c-3a746d04fcb3 \N 76 | 2020-03-18 00:30:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-19 10:40:10.635202+00 2020-03-19 10:40:10.635202+00 completed 61ea1105-4273-41d5-9bf1-ce6bd5a97c74 \N 77 | 2020-03-19 00:15:00+00 sleep \N \N \N \N 26100 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-20 08:49:57.529003+00 2020-03-20 08:49:57.529003+00 completed 7bb83842-1463-4df6-a37a-51331e1ade0e \N 78 | 2020-01-15 22:30:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-01-12 19:12:35.306784+00 2020-03-06 23:51:14.148373+00 2020-01-16 11:22:02.343509+00 completed a40d4254-95fc-43be-9822-f981444642ed \N 79 | 2020-03-21 23:45:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-22 10:13:56.606206+00 2020-03-22 10:13:56.606206+00 completed 7d4f02e4-aef0-4599-93f7-a653930a79ef \N 80 | 2020-03-23 00:45:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-24 10:40:23.490838+00 2020-03-24 10:40:23.490838+00 completed 9208a53d-abb5-4d69-9246-8b76c00f92c5 \N 81 | 2020-02-19 00:30:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-21 11:14:58.529902+00 completed 3d13da56-50bd-4862-aab8-7b16106eb47f \N 82 | 2020-02-20 01:00:00+00 sleep \N \N \N \N 25200 \N \N \N 2020-02-16 13:52:55.624716+00 2020-03-06 23:51:14.148373+00 2020-02-21 11:14:58.529902+00 completed 43dd32da-e35d-4fe2-89c8-672c839ddea6 \N 83 | 2020-02-01 00:00:00+00 Sleep \N \N \N \N 21600 \N \N \N 2020-01-31 10:04:52+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed b3c7866b-1822-463f-9ec4-1393eadb049a \N 84 | 2020-02-02 00:00:00+00 sleep \N \N \N \N 24300 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed 50572aa5-1410-4686-acbf-83b14f55c04c \N 85 | 2020-02-05 23:30:00+00 sleep \N \N \N \N 31500 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed f302f5e2-b8a8-4c61-a000-190760ca22c1 \N 86 | 2020-01-30 00:30:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-01-26 10:02:32.177716+00 2020-03-06 23:51:14.148373+00 2020-02-06 15:07:03.393735+00 completed 5a0d1850-fffc-4b67-a447-ae38041edc9f \N 87 | 2020-02-17 01:30:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-18 18:45:43.603287+00 completed 4866cfdc-2017-426e-9668-2fa38e53ff98 \N 88 | 2020-03-01 01:00:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-02 00:04:08.777682+00 2020-03-02 00:04:08.777682+00 completed 79921eec-6d3b-4f28-b89a-0a82e0746a0d \N 89 | 2020-02-25 00:30:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-01 23:04:34.452674+00 2020-02-28 22:33:42.893966+00 completed e6c5987c-0f8e-46a1-816a-cce7fa163999 \N 90 | 2020-02-25 23:30:00+00 sleep \N \N \N \N 31500 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-01 23:04:34.452674+00 2020-02-28 22:33:42.893966+00 completed 8336a74e-28f8-402f-90dd-6c718105f2f2 \N 91 | 2020-02-29 00:45:00+00 sleep \N \N \N \N 31500 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-01 23:04:34.452674+00 2020-03-01 17:52:05.652893+00 completed 2b2a323f-ba65-447d-b4f6-e4e09e56470f \N 92 | 2020-02-27 01:15:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-01 23:04:34.452674+00 2020-02-28 22:33:42.893966+00 completed 945daf20-615c-4e7c-a2ae-58fb3b6301f2 \N 93 | 2020-03-02 01:00:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-03 13:51:15.744806+00 2020-03-03 13:51:15.744806+00 completed d2460e72-9977-44f8-8263-8cf394a34926 \N 94 | 2020-02-09 00:30:00+00 sleep \N \N \N \N 30600 \N \N \N 2020-01-26 10:48:39.458328+00 2020-03-06 23:51:14.148373+00 2020-02-11 12:50:45.004385+00 completed 060b6f3f-21ea-4e65-a534-f2323b79a922 \N 95 | 2020-02-11 00:30:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-02-09 10:00:52.762642+00 2020-03-06 23:51:14.148373+00 2020-02-14 19:14:47.636024+00 completed df1c09c1-f3f3-48a8-ba75-6995cde1b7e2 \N 96 | 2020-03-17 00:00:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-18 11:48:07.402646+00 2020-03-18 11:48:07.402646+00 completed 9657eb82-ddc3-46dd-9c8d-5f0fb16baa59 \N 97 | 2020-02-27 22:30:00+00 sleep \N \N \N \N 38700 \N \N \N 2020-02-23 12:17:23.115822+00 2020-03-01 23:04:34.452674+00 2020-02-28 22:33:42.893966+00 completed bdf46a79-b7df-458c-b7a9-c334d7f67a21 \N 98 | 2020-03-06 00:30:00+00 sleep \N \N \N \N 25200 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-07 23:51:41.210121+00 2020-03-07 23:51:41.210121+00 completed 87a7947f-99e0-4867-803c-89d6b7ba401f \N 99 | 2020-03-08 00:15:00+00 sleep \N \N \N \N 35100 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-09 19:45:26.830648+00 2020-03-09 19:45:26.830648+00 completed 162c6cb6-3cae-46a6-8951-fa79d5a76b7b \N 100 | 2020-03-09 01:00:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-10 11:50:52.641787+00 2020-03-10 11:50:52.641787+00 completed 53d0d865-4d4c-46df-ade7-002f244a142b \N 101 | 2020-03-03 02:15:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-04 11:55:08.390067+00 2020-03-04 11:55:08.390067+00 completed 71304dde-386a-44a7-a74c-a72f43f70400 \N 102 | 2020-03-05 02:00:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-06 11:12:40.477016+00 2020-03-06 11:12:40.477016+00 completed ff285902-5058-4c94-b58f-04d24ad1e1a0 \N 103 | 2020-03-27 23:45:00+00 Sleep \N \N \N \N 27900 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-29 11:23:42.516813+01 2020-03-29 11:23:42.516813+01 completed c3e96f7c-33ee-40a5-90a7-d4e096b4d173 \N 104 | 2020-03-28 23:30:00+00 Sleep \N \N \N \N 32400 \N \N \N 2020-03-22 23:45:35.247742+00 2020-03-29 11:23:42.516813+01 2020-03-29 11:23:42.516813+01 completed b0d7caf2-160e-4160-8911-686c364159a1 \N 105 | 2020-03-20 23:45:00+00 sleep \N \N \N \N 29700 \N \N \N 2020-03-16 20:52:59.723642+00 2020-03-22 10:13:56.606206+00 2020-03-22 10:13:56.606206+00 completed 655d6cfe-43a1-4180-aacd-5b993a45d136 \N 106 | 2020-03-07 01:00:00+00 sleep \N \N \N \N 32400 \N \N \N 2020-02-29 22:35:27.953055+00 2020-03-08 22:05:21.086403+00 2020-03-08 22:05:21.086403+00 completed ec1c681e-374c-4b03-8c30-faf149acc9f7 \N 107 | 2020-03-12 01:15:00+00 sleep \N \N \N \N 22500 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-13 10:40:32.238625+00 2020-03-13 10:40:32.238625+00 completed 1e91687a-fd2b-4e6f-af81-818feb17e470 \N 108 | 2020-03-13 23:30:00+00 sleep \N \N \N \N 14400 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-14 12:22:57.768616+00 2020-03-14 12:22:57.768616+00 completed 14e584fc-8b7d-4c2f-9c51-9f99dba54ee9 \N 109 | 2020-03-10 00:30:00+00 sleep \N \N \N \N 33300 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-11 11:30:28.079979+00 2020-03-11 11:30:28.079979+00 completed 7ef82286-7551-4296-8d50-8574eca3c46d \N 110 | 2020-03-11 00:30:00+00 sleep \N \N \N \N 28800 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-12 08:35:59.411721+00 2020-03-12 08:35:59.411721+00 completed 7863ab90-3382-4b8e-86ef-beff3f82d78b \N 111 | 2020-03-15 01:15:00+00 sleep \N \N \N \N 27000 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-16 00:59:50.59645+00 2020-03-16 00:59:50.59645+00 completed 436b97eb-7fc7-4243-9ea7-866fa48920b3 \N 112 | 2020-03-16 01:15:00+00 sleep \N \N \N \N 26100 \N \N \N 2020-03-08 22:20:52.368595+00 2020-03-17 09:53:03.269028+00 2020-03-17 09:53:03.269028+00 completed 562b3c32-4175-469f-a8e6-0b51540be4ec \N 113 | -------------------------------------------------------------------------------- /doc/demo/docker/Dockerfile.gentoo: -------------------------------------------------------------------------------- 1 | # name the portage image 2 | FROM gentoo/portage:20200310 as portage 3 | 4 | # image is based on stage3-amd64 5 | FROM gentoo/stage3-amd64-nomultilib:20200310 6 | 7 | # copy the entire portage volume in 8 | COPY --from=portage /var/db/repos/gentoo /var/db/repos/gentoo 9 | 10 | CMD /sbin/init 11 | 12 | # configs, extra service startup scripts etc 13 | COPY ./gentoo/ / 14 | 15 | RUN emerge layman 16 | RUN layman -S 17 | RUN layman -a andrey_utkin 18 | 19 | # --onlydeps is just a convenience measure 20 | # to take advantage of Docker's stage cache. 21 | # Plenty of heavyweight dependencies don't change much. 22 | # taskdb itself changes a lot, so install it on the last step. 23 | RUN emerge --onlydeps app-misc/taskdb 24 | RUN rc-update add postgresql-12 default 25 | 26 | # optional runtime deps 27 | RUN emerge www-apps/grafana-bin 28 | RUN rc-update add grafana default 29 | 30 | RUN emerge www-servers/nginx 31 | RUN rc-update add nginx default 32 | RUN mkdir -p /var/www/taskdb/htdocs 33 | RUN sed -i -e 's/listen 127.0.0.1;/listen 0.0.0.0;/' -e 's|root /var/www/localhost/htdocs;|root /var/www/taskdb/htdocs;|' /etc/nginx/nginx.conf 34 | # Work around 'net' service being not up from openrc point of view 35 | RUN sed -i -e 's/need net/#&/' /etc/init.d/nginx 36 | 37 | # demo enablement 38 | RUN emerge app-admin/pass app-editors/vim 39 | # optional convenience 40 | RUN emerge app-misc/tmux 41 | 42 | # installation of unpackaged components: OmniDB 43 | RUN emerge dev-python/pip 44 | RUN wget https://github.com/OmniDB/OmniDB/archive/2.17.0.tar.gz -O OmniDB-2.17.0.tar.gz 45 | RUN tar xzf OmniDB-2.17.0.tar.gz 46 | RUN ln -sv OmniDB-2.17.0 OmniDB 47 | # avoid fetching and building unnecessary dependencies for other RDBMS 48 | RUN cd OmniDB && grep -v 'psycopg\|pycparser\|cx_Oracle\|PyMySQL' requirements.txt > myreq.txt && pip3 install --user -r ./myreq.txt 49 | 50 | # Finally, install the product 51 | RUN emerge app-misc/taskdb 52 | 53 | # Provision an out of the box setup 54 | RUN emerge --config postgresql 55 | -------------------------------------------------------------------------------- /doc/demo/docker/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build --tag taskdb-gentoo . -f Dockerfile.gentoo 3 | 4 | .PHONY: build 5 | -------------------------------------------------------------------------------- /doc/demo/docker/gentoo/etc/local.d/omnidb.start: -------------------------------------------------------------------------------- 1 | pushd /OmniDB/OmniDB || exit 1 2 | ./omnidb-server.py --host 0.0.0.0 & 3 | popd 4 | -------------------------------------------------------------------------------- /doc/demo/docker/gentoo/etc/portage/make.conf: -------------------------------------------------------------------------------- 1 | # These settings were set by the catalyst build script that automatically 2 | # built this stage. 3 | # Please consult /usr/share/portage/config/make.conf.example for a more 4 | # detailed example. 5 | COMMON_FLAGS="-O2 -pipe" 6 | CFLAGS="${COMMON_FLAGS}" 7 | CXXFLAGS="${COMMON_FLAGS}" 8 | FCFLAGS="${COMMON_FLAGS}" 9 | FFLAGS="${COMMON_FLAGS}" 10 | 11 | # NOTE: This stage was built with the bindist Use flag enabled 12 | PORTDIR="/var/db/repos/gentoo" 13 | DISTDIR="/var/cache/distfiles" 14 | PKGDIR="/var/cache/binpkgs" 15 | 16 | # This sets the language of build output to English. 17 | # Please keep this setting intact when reporting bugs. 18 | LC_MESSAGES=C 19 | 20 | # Above is verbatim make.conf from Docker container gentoo/stage3-amd64-nomultilib:20200310 21 | # Below is bespoke addition for taskdb container. 22 | 23 | EMERGE_DEFAULT_OPTS="--verbose --quiet-build --quiet-fail --autounmask=y --autounmask-write=y --autounmask-continue=y --autounmask-keep-keywords=n" 24 | # Sandboxing fails in Docker, and just prints annoying warnings. 25 | FEATURES="-sandbox -ipc-sandbox -pid-sandbox -network-sandbox" 26 | USE="-introspection" 27 | -------------------------------------------------------------------------------- /doc/demo/docker/gentoo/etc/portage/package.accept_keywords: -------------------------------------------------------------------------------- 1 | app-misc/taskdb ** 2 | -------------------------------------------------------------------------------- /doc/demo/docker/gentoo/etc/portage/package.use: -------------------------------------------------------------------------------- 1 | www-servers/nginx nginx_modules_http_fancyindex 2 | -------------------------------------------------------------------------------- /doc/demo/docker/launch-container: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONTAINER_ID=$( 4 | docker run \ 5 | -d \ 6 | -p 127.0.0.1:3000:3000 \ 7 | -p 127.0.0.1:8000:8000 \ 8 | -p 127.0.0.1:25482:25482 \ 9 | -p 127.0.0.1:5432:5432 \ 10 | -p 127.0.0.1:80:80 \ 11 | andreyutkin/taskdb-provisioned:latest 12 | ) 13 | 14 | docker exec -it $CONTAINER_ID /bin/bash -i 15 | 16 | docker kill $CONTAINER_ID 17 | -------------------------------------------------------------------------------- /doc/taskdb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-utkin/taskdb/d9e592be19e516015894e5c1661f28fc7847692c/doc/taskdb_logo.png -------------------------------------------------------------------------------- /share/auto-tagging/edit_and_exec: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -e 3 | 4 | if [[ $# != 1 ]]; then 5 | echo "Usage: $0 " 6 | fi 7 | 8 | SUGGESTION_DIR=$1 9 | # FIXME: security: use private tmpdir 10 | SUGGESTED_COMMANDS_FILE=$(mktemp taskdb.auto-tagging.XXXXXXX) 11 | ./suggest "$SUGGESTION_DIR" > "$SUGGESTED_COMMANDS_FILE" 12 | 13 | if ! [[ -s "$SUGGESTED_COMMANDS_FILE" ]]; then 14 | rm "$SUGGESTED_COMMANDS_FILE" 15 | exit 0 16 | fi 17 | 18 | # * Launch $EDITOR on this file 19 | # Abnormal exit aborts the script (e.g. use vim's ":cq") 20 | $EDITOR "$SUGGESTED_COMMANDS_FILE" 21 | if [[ "$?" != 0 ]]; then 22 | rm "$SUGGESTED_COMMANDS_FILE" 23 | exit 1 24 | fi 25 | 26 | # * Feed the file contents into `psql` 27 | psql < "$SUGGESTED_COMMANDS_FILE" 28 | 29 | rm "$SUGGESTED_COMMANDS_FILE" 30 | -------------------------------------------------------------------------------- /share/auto-tagging/run_all: -------------------------------------------------------------------------------- 1 | for x in suggestions/*; do ./edit_and_exec $x || break; done 2 | -------------------------------------------------------------------------------- /share/auto-tagging/suggest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Traverse matching expressions files, in alphanum order. For each: 4 | # * Run SELECT uuid, descr, scheduled FROM tasks WHERE AND AND status in ('pending', 'completed') 5 | # * Format "UPDATE SET WHERE uuid in " 6 | 7 | """ 8 | """ 9 | 10 | import os 11 | import sys 12 | import subprocess 13 | import psycopg2 14 | import psycopg2.extras 15 | from datetime import datetime, timedelta 16 | import pytz 17 | 18 | suggestion_dir = sys.argv[1] 19 | 20 | with open('{home}/.taskdb/conn_string_py'.format(home=os.getenv("HOME")), 'r') as file: 21 | conn_string = file.read() 22 | 23 | conn = psycopg2.connect(conn_string) 24 | cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 25 | 26 | with open(suggestion_dir + '/cond.sql.inc', 'r') as cond_file: 27 | cond = cond_file.read() 28 | with open(suggestion_dir + '/set.sql.inc', 'r') as set_file: 29 | set_sql_code = set_file.read() 30 | 31 | # TODO use BETWEEN? 32 | #date_range_cond = "scheduled::date >= '2019-08-12' AND scheduled::date < '2019-08-26'" 33 | date_range_cond = "scheduled::date >= '2019-08-12'" 34 | 35 | query = f"SELECT * FROM tasks AS t WHERE {cond} AND {date_range_cond} AND t.status in ('pending', 'completed') ORDER BY scheduled" 36 | 37 | cur.execute(query) 38 | 39 | if cur.rowcount == 0: 40 | sys.exit(0) 41 | 42 | tagging_query = f'UPDATE tasks AS t SET \n{set_sql_code} WHERE t.uuid IN (' 43 | 44 | while True: 45 | row = cur.fetchone() 46 | if not row: 47 | break 48 | 49 | tagging_query += "\n'" + row['uuid'] + "', -- " + str(row['scheduled']) + ' ' + row['description'] 50 | 51 | # NULL is needed to accomodate trailing comma in last entry 52 | tagging_query += '\nNULL\n);' 53 | 54 | print(tagging_query) 55 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/01_completed_cleanup/cond.sql.inc: -------------------------------------------------------------------------------- 1 | status = 'pending' 2 | AND scheduled::date <= (CURRENT_TIMESTAMP - '1 day'::interval)::date 3 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/01_completed_cleanup/set.sql.inc: -------------------------------------------------------------------------------- 1 | status = 'completed' 2 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/10_link_to_activity_by_descr_match/cond.sql.inc: -------------------------------------------------------------------------------- 1 | parent IS NULL 2 | AND (project IS NULL OR project = '') 3 | AND scheduled >= '2020-02-24' 4 | AND EXISTS ( 5 | SELECT parent_candidate.uuid 6 | FROM tasks AS parent_candidate 7 | WHERE 8 | parent_candidate.tags ~ 'category|megatask' 9 | AND parent_candidate.uuid != t.uuid 10 | AND LOWER(parent_candidate.description) = LOWER(t.description) 11 | ) 12 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/10_link_to_activity_by_descr_match/set.sql.inc: -------------------------------------------------------------------------------- 1 | parent = ( 2 | SELECT parent_candidate.uuid 3 | FROM tasks AS parent_candidate 4 | WHERE 5 | parent_candidate.tags ~ 'category|megatask' 6 | AND parent_candidate.uuid != t.uuid 7 | AND LOWER(parent_candidate.description) = LOWER(t.description) 8 | ) 9 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/19_extract_alias_from_descr/cond.sql.inc: -------------------------------------------------------------------------------- 1 | alias IS NULL 2 | AND COALESCE(duration, 0) = 0 3 | AND description ~ '[[]([^\[\]]+)[]]$' 4 | AND scheduled > '2020-03-20' 5 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/19_extract_alias_from_descr/set.sql.inc: -------------------------------------------------------------------------------- 1 | alias = substring(description, '[[]([^\[\]]+)[]]$') 2 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/20_link_to_parent_by_alias_in_project/cond.sql.inc: -------------------------------------------------------------------------------- 1 | parent IS NULL 2 | AND project IS NOT NULL 3 | AND scheduled >= '2020-02-24' 4 | AND EXISTS ( 5 | SELECT parent_candidate.uuid 6 | FROM tasks AS parent_candidate 7 | WHERE 8 | parent_candidate.uuid != t.uuid 9 | AND parent_candidate.alias IS NOT NULL 10 | AND parent_candidate.alias != '' 11 | AND parent_candidate.alias = t.project 12 | ) 13 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/20_link_to_parent_by_alias_in_project/set.sql.inc: -------------------------------------------------------------------------------- 1 | parent = ( 2 | SELECT parent_candidate.uuid 3 | FROM tasks AS parent_candidate 4 | WHERE 5 | parent_candidate.uuid != t.uuid 6 | AND parent_candidate.alias IS NOT NULL 7 | AND parent_candidate.alias != '' 8 | AND parent_candidate.alias = t.project 9 | ) 10 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/30_link_to_parent_by_alias_in_descr_prefix/cond.sql.inc: -------------------------------------------------------------------------------- 1 | parent IS NULL 2 | AND (project IS NULL OR project = '') 3 | AND scheduled >= '2020-02-24' 4 | AND EXISTS ( 5 | SELECT parent_candidate.uuid 6 | FROM tasks AS parent_candidate 7 | WHERE 8 | parent_candidate.uuid != t.uuid 9 | AND parent_candidate.alias IS NOT NULL 10 | AND parent_candidate.alias != '' 11 | AND t.description ~* ('^' || parent_candidate.alias || '([ :]|$)') 12 | ) 13 | -------------------------------------------------------------------------------- /share/auto-tagging/suggestions/30_link_to_parent_by_alias_in_descr_prefix/set.sql.inc: -------------------------------------------------------------------------------- 1 | parent = ( 2 | SELECT parent_candidate.uuid 3 | FROM tasks AS parent_candidate 4 | WHERE 5 | parent_candidate.uuid != t.uuid 6 | AND parent_candidate.alias IS NOT NULL 7 | AND parent_candidate.alias != '' 8 | AND t.description ~* ('^' || parent_candidate.alias || '([ :]|$)') 9 | ) 10 | -------------------------------------------------------------------------------- /share/functions/alias.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION alias(alias_ text) 2 | RETURNS uuid 3 | LANGUAGE SQL 4 | AS $$ 5 | SELECT uuid FROM tasks WHERE tasks.alias = alias_ 6 | $$ 7 | -------------------------------------------------------------------------------- /share/functions/depgraph.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | 3 | -- SELECT * FROM depgraph() 4 | 5 | -- SELECT value 6 | -- FROM graph((SELECT ARRAY_AGG(depgraph) FROM depgraph())) 7 | -- ORDER BY order_ 8 | 9 | DROP FUNCTION depgraph(uuid); 10 | CREATE OR REPLACE FUNCTION depgraph(head_uuid uuid) 11 | RETURNS SETOF tasks 12 | LANGUAGE SQL 13 | AS $$ 14 | 15 | WITH RECURSIVE depgraph AS 16 | ( 17 | ( 18 | SELECT * 19 | FROM tasks 20 | WHERE tasks.uuid = head_uuid 21 | ) 22 | UNION ALL 23 | ( 24 | SELECT n.* 25 | FROM tasks AS n, depgraph AS r 26 | WHERE ( 27 | n.parent = r.uuid 28 | OR 29 | r.dependencies ~ n.uuid::text 30 | ) 31 | AND n.status IN ('pending', 'completed') 32 | ) 33 | ) 34 | SELECT * FROM depgraph 35 | $$ 36 | -------------------------------------------------------------------------------- /share/functions/depgraph_root_to_selection.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | -- 3 | -- SELECT value FROM graph(( 4 | -- SELECT ARRAY_AGG(depgraph_root_to_selection) FROM depgraph_root_to_selection 5 | -- ( 6 | -- (SELECT uuid FROM tasks WHERE alias = 'root'), 7 | -- ( 8 | -- SELECT array_agg(tasks) FROM tasks 9 | -- WHERE 10 | -- status IN ('pending', 'completed') 11 | -- AND scheduled >= date_trunc('day', current_date) + INTERVAL '7:30:00' 12 | -- AND scheduled < date_trunc('day', current_date) + INTERVAL '1 days' 13 | -- ) 14 | -- ) 15 | -- 16 | -- )) 17 | -- ORDER BY order_ 18 | -- 19 | -- psql -qtAX -f query.sql > graph.dot 20 | -- dot -Tpng graph.dot -o graph.png 21 | 22 | CREATE OR REPLACE FUNCTION depgraph_root_to_selection(root uuid, selection_array tasks[]) 23 | RETURNS SETOF tasks 24 | AS $$ 25 | 26 | WITH 27 | minimal_dataset AS ( 28 | SELECT * FROM unnest(selection_array) 29 | ), 30 | 31 | conservative_dataset AS ( 32 | SELECT * FROM depgraph(root) 33 | ), 34 | 35 | challenged_subset AS ( 36 | SELECT * FROM conservative_dataset 37 | EXCEPT 38 | SELECT * FROM minimal_dataset 39 | ), 40 | 41 | data AS ( 42 | SELECT * FROM minimal_dataset 43 | UNION 44 | SELECT * FROM challenged_subset 45 | WHERE EXISTS( 46 | SELECT * FROM depgraph(challenged_subset.uuid) 47 | INTERSECT 48 | SELECT * FROM minimal_dataset 49 | ) 50 | ) 51 | 52 | SELECT * FROM data 53 | 54 | $$ 55 | LANGUAGE SQL 56 | -------------------------------------------------------------------------------- /share/functions/deps.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | 3 | -- SELECT * FROM deps() 4 | 5 | DROP FUNCTION deps(uuid); 6 | CREATE OR REPLACE FUNCTION deps(arg uuid) 7 | RETURNS SETOF tasks 8 | LANGUAGE SQL 9 | AS $$ 10 | 11 | WITH strictly_deps AS ( 12 | SELECT dependencies FROM tasks WHERE uuid = arg 13 | ) 14 | 15 | SELECT n.* 16 | FROM tasks AS n 17 | WHERE 18 | n.status IN ('pending', 'completed') 19 | AND n.parent = arg 20 | 21 | UNION 22 | 23 | SELECT n.* 24 | FROM tasks as n, strictly_deps 25 | WHERE 26 | n.status IN ('pending', 'completed') 27 | AND n.uuid::text IN (SELECT unnest(string_to_array(strictly_deps.dependencies, E'\n')) AS uuid) 28 | 29 | $$ 30 | -------------------------------------------------------------------------------- /share/functions/graph.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | -- 3 | -- SELECT value 4 | -- FROM graph((SELECT ARRAY_AGG(tasks) FROM tasks WHERE )) 5 | -- ORDER BY order_ 6 | -- 7 | -- psql -qtAX -f query.sql > graph.dot 8 | -- dot -Tpng graph.dot -o graph.png 9 | 10 | CREATE OR REPLACE FUNCTION graph(selection_array tasks[]) 11 | RETURNS TABLE (value text, order_ int) 12 | AS $$ 13 | 14 | WITH 15 | selection AS ( 16 | SELECT * FROM unnest(selection_array.*) 17 | ), 18 | nodes AS ( 19 | SELECT graph_node_repr(selection) AS value, 1 AS order_ 20 | FROM selection 21 | ), 22 | edges AS ( 23 | SELECT '"' || s1.uuid || '" -> "' || dep.uuid || '"' AS value, 2 AS order_ 24 | FROM selection s1, 25 | LATERAL ( 26 | SELECT unnest(string_to_array(s1.dependencies, E'\n')) AS uuid 27 | INTERSECT 28 | SELECT uuid::text FROM selection AS uuid 29 | ) AS dep 30 | UNION 31 | SELECT '"' || selection.parent || '" -> "' || selection.uuid || '"' AS value, 2 AS order_ 32 | FROM selection 33 | WHERE selection.parent IN (SELECT uuid FROM selection) 34 | ), 35 | full_graph AS ( 36 | SELECT 'digraph G {' AS value, 0 AS order_ 37 | UNION 38 | SELECT * FROM nodes 39 | UNION 40 | SELECT * FROM edges 41 | UNION 42 | SELECT '}' AS value, 3 AS order_ 43 | ) 44 | 45 | SELECT * FROM full_graph ORDER BY order_ 46 | 47 | $$ 48 | LANGUAGE SQL 49 | -------------------------------------------------------------------------------- /share/functions/graph_node_repr.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | -- 3 | -- SELECT value 4 | -- FROM graph((SELECT ARRAY_AGG(tasks) FROM tasks WHERE )) 5 | -- ORDER BY order_ 6 | -- 7 | -- psql -qtAX -f query.sql > graph.dot 8 | -- dot -Tpng graph.dot -o graph.png 9 | 10 | CREATE OR REPLACE FUNCTION graph_node_repr(t tasks) 11 | RETURNS text 12 | AS $$ 13 | 14 | WITH status AS ( 15 | SELECT 16 | CASE 17 | WHEN t.scheduled IS NULL 18 | THEN 'unscheduled' 19 | WHEN t.scheduled IS NOT NULL 20 | AND (t.scheduled + COALESCE(t.duration, 0) * '1 second'::interval) < NOW() 21 | THEN 'completed' 22 | WHEN t.scheduled IS NOT NULL 23 | AND NOW() BETWEEN t.scheduled AND (t.scheduled + COALESCE(t.duration, 0) * '1 second'::interval) 24 | THEN 'current' 25 | ELSE 'pending' 26 | END 27 | AS status 28 | ), 29 | 30 | repr AS ( 31 | SELECT 32 | t.uuid, 33 | COALESCE(t.alias, REPLACE(t.description, '"', E'\\"')) AS label, 34 | -- color 35 | CASE 36 | WHEN status.status = 'unscheduled' 37 | THEN 'grey' 38 | WHEN status.status = 'completed' 39 | THEN 'green' 40 | WHEN status.status = 'current' 41 | THEN 'orange' 42 | WHEN status.status = 'pending' 43 | THEN 'red' 44 | END 45 | AS color 46 | FROM status 47 | ) 48 | 49 | SELECT 50 | '"' || repr.uuid || '" [label="' || repr.label || '" color="' || repr.color || '"]' 51 | FROM repr 52 | 53 | $$ 54 | LANGUAGE SQL 55 | -------------------------------------------------------------------------------- /share/functions/priority_level.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION priority_level(priority text) RETURNS int 2 | LANGUAGE sql 3 | AS $$ 4 | SELECT 5 | CASE priority 6 | WHEN 'customer-blocked' THEN 9 7 | WHEN 'typical-usecase-blocked' THEN 8 8 | 9 | WHEN 'commitment' THEN 7 10 | WHEN 'quality-strategy' THEN 6 11 | 12 | WHEN 'growth-blocked' THEN 5 13 | WHEN 'unique-usecase-blocked' THEN 4 14 | 15 | WHEN 'customer-value-improvement' THEN 3 16 | WHEN 'internal-productivity' THEN 2 17 | 18 | WHEN 'workaround-required' THEN 1 19 | ELSE 0 20 | END 21 | $$; 22 | -------------------------------------------------------------------------------- /share/functions/rdeps.sql: -------------------------------------------------------------------------------- 1 | -- Usage: 2 | 3 | -- SELECT * FROM rdeps() 4 | 5 | DROP FUNCTION rdeps(uuid); 6 | CREATE OR REPLACE FUNCTION rdeps(arg uuid) 7 | RETURNS SETOF tasks 8 | LANGUAGE SQL 9 | AS $$ 10 | 11 | WITH arg_task AS ( 12 | SELECT parent FROM tasks WHERE uuid = arg 13 | ) 14 | 15 | SELECT n.* 16 | FROM tasks AS n, arg_task 17 | WHERE 18 | n.status IN ('pending', 'completed') 19 | AND uuid = arg_task.parent 20 | 21 | UNION 22 | 23 | SELECT n.* 24 | FROM tasks as n 25 | WHERE 26 | n.status IN ('pending', 'completed') 27 | AND arg::text IN (SELECT unnest(string_to_array(n.dependencies, E'\n')) AS uuid) 28 | 29 | $$ 30 | -------------------------------------------------------------------------------- /share/schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 11.4 6 | -- Dumped by pg_dump version 11.4 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | -- 20 | -- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - 21 | -- 22 | 23 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; 24 | 25 | 26 | -- 27 | -- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: - 28 | -- 29 | 30 | COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; 31 | 32 | 33 | -- 34 | -- Name: task_status; Type: TYPE; Schema: public; Owner: - 35 | -- 36 | 37 | CREATE TYPE public.task_status AS ENUM ( 38 | 'completed', 39 | 'pending', 40 | 'recurring', 41 | 'deleted', 42 | 'waiting', 43 | 'cancelled' 44 | ); 45 | 46 | 47 | -- 48 | -- Name: alias(text); Type: FUNCTION; Schema: public; Owner: - 49 | -- 50 | 51 | CREATE FUNCTION public.alias(alias_ text) RETURNS uuid 52 | LANGUAGE sql 53 | AS $$ 54 | SELECT uuid FROM tasks WHERE tasks.alias = alias_ 55 | $$; 56 | 57 | 58 | -- 59 | -- Name: changes_notify_fn(); Type: FUNCTION; Schema: public; Owner: - 60 | -- 61 | 62 | CREATE FUNCTION public.changes_notify_fn() RETURNS trigger 63 | LANGUAGE plpgsql 64 | AS $$ 65 | BEGIN 66 | PERFORM pg_notify('CHANGES', substring(replace(current_query(), E'\n', '\n') FROM 0 FOR 8000)); 67 | RETURN NEW; 68 | END; 69 | $$; 70 | 71 | 72 | SET default_tablespace = ''; 73 | 74 | SET default_with_oids = false; 75 | 76 | -- 77 | -- Name: tasks; Type: TABLE; Schema: public; Owner: - 78 | -- 79 | 80 | CREATE TABLE public.tasks ( 81 | scheduled timestamp with time zone, 82 | description text, 83 | annotation text, 84 | project text, 85 | priority text, 86 | due timestamp with time zone, 87 | duration integer, 88 | tags text, 89 | parent uuid, 90 | dependencies text, 91 | entry timestamp with time zone DEFAULT CURRENT_TIMESTAMP, 92 | modified timestamp with time zone DEFAULT CURRENT_TIMESTAMP, 93 | ended timestamp with time zone, 94 | status public.task_status DEFAULT 'pending'::public.task_status NOT NULL, 95 | uuid uuid DEFAULT public.uuid_generate_v4() NOT NULL, 96 | alias text 97 | ); 98 | 99 | 100 | -- 101 | -- Name: depgraph(uuid); Type: FUNCTION; Schema: public; Owner: - 102 | -- 103 | 104 | CREATE FUNCTION public.depgraph(head_uuid uuid) RETURNS SETOF public.tasks 105 | LANGUAGE sql 106 | AS $$ 107 | 108 | WITH RECURSIVE depgraph AS 109 | ( 110 | ( 111 | SELECT * 112 | FROM tasks 113 | WHERE tasks.uuid = head_uuid 114 | ) 115 | UNION ALL 116 | ( 117 | SELECT n.* 118 | FROM tasks AS n, depgraph AS r 119 | WHERE ( 120 | n.parent = r.uuid 121 | OR 122 | r.dependencies ~ n.uuid::text 123 | ) 124 | AND n.status IN ('pending', 'completed') 125 | ) 126 | ) 127 | SELECT * FROM depgraph 128 | $$; 129 | 130 | 131 | -- 132 | -- Name: depgraph_root_to_selection(uuid, public.tasks[]); Type: FUNCTION; Schema: public; Owner: - 133 | -- 134 | 135 | CREATE FUNCTION public.depgraph_root_to_selection(root uuid, selection_array public.tasks[]) RETURNS SETOF public.tasks 136 | LANGUAGE sql 137 | AS $$ 138 | 139 | WITH 140 | minimal_dataset AS ( 141 | SELECT * FROM unnest(selection_array) 142 | ), 143 | 144 | conservative_dataset AS ( 145 | SELECT * FROM depgraph(root) 146 | ), 147 | 148 | challenged_subset AS ( 149 | SELECT * FROM conservative_dataset 150 | EXCEPT 151 | SELECT * FROM minimal_dataset 152 | ), 153 | 154 | data AS ( 155 | SELECT * FROM minimal_dataset 156 | UNION 157 | SELECT * FROM challenged_subset 158 | WHERE EXISTS( 159 | SELECT * FROM depgraph(challenged_subset.uuid) 160 | INTERSECT 161 | SELECT * FROM minimal_dataset 162 | ) 163 | ) 164 | 165 | SELECT * FROM data 166 | 167 | $$; 168 | 169 | 170 | -- 171 | -- Name: deps(uuid); Type: FUNCTION; Schema: public; Owner: - 172 | -- 173 | 174 | CREATE FUNCTION public.deps(arg uuid) RETURNS SETOF public.tasks 175 | LANGUAGE sql 176 | AS $$ 177 | 178 | WITH strictly_deps AS ( 179 | SELECT dependencies FROM tasks WHERE uuid = arg 180 | ) 181 | 182 | SELECT n.* 183 | FROM tasks AS n 184 | WHERE 185 | n.status IN ('pending', 'completed') 186 | AND n.parent = arg 187 | 188 | UNION 189 | 190 | SELECT n.* 191 | FROM tasks as n, strictly_deps 192 | WHERE 193 | n.status IN ('pending', 'completed') 194 | AND n.uuid::text IN (SELECT unnest(string_to_array(strictly_deps.dependencies, E'\n')) AS uuid) 195 | 196 | $$; 197 | 198 | 199 | -- 200 | -- Name: graph(public.tasks[]); Type: FUNCTION; Schema: public; Owner: - 201 | -- 202 | 203 | CREATE FUNCTION public.graph(selection_array public.tasks[]) RETURNS TABLE(value text, order_ integer) 204 | LANGUAGE sql 205 | AS $$ 206 | 207 | WITH 208 | selection AS ( 209 | SELECT * FROM unnest(selection_array.*) 210 | ), 211 | nodes AS ( 212 | SELECT graph_node_repr(selection) AS value, 1 AS order_ 213 | FROM selection 214 | ), 215 | edges AS ( 216 | SELECT '"' || s1.uuid || '" -> "' || dep.uuid || '"' AS value, 2 AS order_ 217 | FROM selection s1, 218 | LATERAL ( 219 | SELECT unnest(string_to_array(s1.dependencies, E'\n')) AS uuid 220 | INTERSECT 221 | SELECT uuid::text FROM selection AS uuid 222 | ) AS dep 223 | UNION 224 | SELECT '"' || selection.parent || '" -> "' || selection.uuid || '"' AS value, 2 AS order_ 225 | FROM selection 226 | WHERE selection.parent IN (SELECT uuid FROM selection) 227 | ), 228 | full_graph AS ( 229 | SELECT 'digraph G {' AS value, 0 AS order_ 230 | UNION 231 | SELECT * FROM nodes 232 | UNION 233 | SELECT * FROM edges 234 | UNION 235 | SELECT '}' AS value, 3 AS order_ 236 | ) 237 | 238 | SELECT * FROM full_graph ORDER BY order_ 239 | 240 | $$; 241 | 242 | 243 | -- 244 | -- Name: graph_node_repr(public.tasks); Type: FUNCTION; Schema: public; Owner: - 245 | -- 246 | 247 | CREATE FUNCTION public.graph_node_repr(t public.tasks) RETURNS text 248 | LANGUAGE sql 249 | AS $$ 250 | 251 | WITH status AS ( 252 | SELECT 253 | CASE 254 | WHEN t.scheduled IS NULL 255 | THEN 'unscheduled' 256 | WHEN t.scheduled IS NOT NULL 257 | AND (t.scheduled + COALESCE(t.duration, 0) * '1 second'::interval) < NOW() 258 | THEN 'completed' 259 | WHEN t.scheduled IS NOT NULL 260 | AND NOW() BETWEEN t.scheduled AND (t.scheduled + COALESCE(t.duration, 0) * '1 second'::interval) 261 | THEN 'current' 262 | ELSE 'pending' 263 | END 264 | AS status 265 | ), 266 | 267 | repr AS ( 268 | SELECT 269 | t.uuid, 270 | COALESCE(t.alias, REPLACE(t.description, '"', E'\\"')) AS label, 271 | -- color 272 | CASE 273 | WHEN status.status = 'unscheduled' 274 | THEN 'grey' 275 | WHEN status.status = 'completed' 276 | THEN 'green' 277 | WHEN status.status = 'current' 278 | THEN 'orange' 279 | WHEN status.status = 'pending' 280 | THEN 'red' 281 | END 282 | AS color 283 | FROM status 284 | ) 285 | 286 | SELECT 287 | '"' || repr.uuid || '" [label="' || repr.label || '" color="' || repr.color || '"]' 288 | FROM repr 289 | 290 | $$; 291 | 292 | 293 | -- 294 | -- Name: rdeps(uuid); Type: FUNCTION; Schema: public; Owner: - 295 | -- 296 | 297 | CREATE FUNCTION public.rdeps(arg uuid) RETURNS SETOF public.tasks 298 | LANGUAGE sql 299 | AS $$ 300 | 301 | WITH arg_task AS ( 302 | SELECT parent FROM tasks WHERE uuid = arg 303 | ) 304 | 305 | SELECT n.* 306 | FROM tasks AS n, arg_task 307 | WHERE 308 | n.status IN ('pending', 'completed') 309 | AND uuid = arg_task.parent 310 | 311 | UNION 312 | 313 | SELECT n.* 314 | FROM tasks as n 315 | WHERE 316 | n.status IN ('pending', 'completed') 317 | AND arg::text IN (SELECT unnest(string_to_array(n.dependencies, E'\n')) AS uuid) 318 | 319 | $$; 320 | 321 | 322 | -- 323 | -- Name: update_ended_fn(); Type: FUNCTION; Schema: public; Owner: - 324 | -- 325 | 326 | CREATE FUNCTION public.update_ended_fn() RETURNS trigger 327 | LANGUAGE plpgsql 328 | AS $$ 329 | BEGIN 330 | IF NEW.status in ('completed', 'deleted', 'cancelled') and (TG_OP = 'INSERT' or OLD.status != NEW.status) THEN 331 | NEW.ended := CURRENT_TIMESTAMP; 332 | END IF; 333 | RETURN NEW; 334 | END; 335 | $$; 336 | 337 | 338 | -- 339 | -- Name: update_modified_fn(); Type: FUNCTION; Schema: public; Owner: - 340 | -- 341 | 342 | CREATE FUNCTION public.update_modified_fn() RETURNS trigger 343 | LANGUAGE plpgsql 344 | AS $$ 345 | BEGIN 346 | IF (NEW.modified IS NOT DISTINCT FROM OLD.modified) THEN 347 | NEW.modified := CURRENT_TIMESTAMP; 348 | END IF; 349 | RETURN NEW; 350 | END; 351 | $$; 352 | 353 | 354 | -- 355 | -- Name: conflicting; Type: VIEW; Schema: public; Owner: - 356 | -- 357 | 358 | CREATE VIEW public.conflicting AS 359 | SELECT t.scheduled, 360 | t.description, 361 | t.annotation, 362 | t.project, 363 | t.priority, 364 | t.due, 365 | t.duration, 366 | t.tags, 367 | t.parent, 368 | t.dependencies, 369 | t.entry, 370 | t.modified, 371 | t.ended, 372 | t.status, 373 | t.uuid, 374 | t.alias 375 | FROM public.tasks t 376 | WHERE ((t.scheduled IS NOT NULL) AND (t.status = 'pending'::public.task_status) AND (t.duration > 0) AND (EXISTS ( SELECT another_task.scheduled, 377 | another_task.description, 378 | another_task.annotation, 379 | another_task.project, 380 | another_task.priority, 381 | another_task.due, 382 | another_task.duration, 383 | another_task.tags, 384 | another_task.parent, 385 | another_task.dependencies, 386 | another_task.entry, 387 | another_task.modified, 388 | another_task.ended, 389 | another_task.status, 390 | another_task.uuid, 391 | another_task.alias 392 | FROM public.tasks another_task 393 | WHERE ((another_task.uuid <> t.uuid) AND (another_task.status = 'pending'::public.task_status) AND (another_task.duration > 0) AND (t.scheduled < (another_task.scheduled + ((another_task.duration)::double precision * '00:00:01'::interval))) AND ((t.scheduled + ((t.duration)::double precision * '00:00:01'::interval)) > another_task.scheduled)) 394 | LIMIT 1))) 395 | ORDER BY t.scheduled; 396 | 397 | 398 | -- 399 | -- Name: megatasks; Type: VIEW; Schema: public; Owner: - 400 | -- 401 | 402 | CREATE VIEW public.megatasks AS 403 | SELECT megatask.scheduled, 404 | megatask.description, 405 | megatask.annotation, 406 | megatask.project, 407 | megatask.priority, 408 | megatask.due, 409 | megatask.duration, 410 | megatask.tags, 411 | megatask.parent, 412 | megatask.dependencies, 413 | megatask.entry, 414 | megatask.modified, 415 | megatask.ended, 416 | megatask.status, 417 | megatask.uuid, 418 | megatask.alias 419 | FROM public.tasks megatask 420 | WHERE ((megatask.status = 'pending'::public.task_status) AND (megatask.scheduled IS NOT NULL) AND (COALESCE(megatask.duration, 0) = 0) AND (EXISTS ( SELECT public.deps(megatask.uuid) AS deps)) AND (NOT (EXISTS ( SELECT parent.scheduled, 421 | parent.description, 422 | parent.annotation, 423 | parent.project, 424 | parent.priority, 425 | parent.due, 426 | parent.duration, 427 | parent.tags, 428 | parent.parent, 429 | parent.dependencies, 430 | parent.entry, 431 | parent.modified, 432 | parent.ended, 433 | parent.status, 434 | parent.uuid, 435 | parent.alias 436 | FROM public.rdeps(megatask.uuid) parent(scheduled, description, annotation, project, priority, due, duration, tags, parent, dependencies, entry, modified, ended, status, uuid, alias) 437 | WHERE (parent.scheduled IS NOT NULL))))) 438 | ORDER BY megatask.scheduled; 439 | 440 | 441 | -- 442 | -- Name: overdue; Type: VIEW; Schema: public; Owner: - 443 | -- 444 | 445 | CREATE VIEW public.overdue AS 446 | SELECT tasks.scheduled, 447 | tasks.description, 448 | tasks.annotation, 449 | tasks.project, 450 | tasks.priority, 451 | tasks.due, 452 | tasks.duration, 453 | tasks.tags, 454 | tasks.parent, 455 | tasks.dependencies, 456 | tasks.entry, 457 | tasks.modified, 458 | tasks.ended, 459 | tasks.status, 460 | tasks.uuid, 461 | tasks.alias 462 | FROM public.tasks 463 | WHERE ((tasks.status = 'pending'::public.task_status) AND (((tasks.scheduled + ((COALESCE(tasks.duration, 0))::double precision * '00:00:01'::interval)) < CURRENT_TIMESTAMP) OR (tasks.due < CURRENT_TIMESTAMP))) 464 | ORDER BY tasks.scheduled; 465 | 466 | 467 | -- 468 | -- Name: tdt; Type: VIEW; Schema: public; Owner: - 469 | -- 470 | 471 | CREATE VIEW public.tdt AS 472 | SELECT tasks.scheduled, 473 | tasks.description, 474 | tasks.annotation, 475 | tasks.project, 476 | tasks.priority, 477 | tasks.due, 478 | tasks.duration, 479 | tasks.tags, 480 | tasks.parent, 481 | tasks.dependencies, 482 | tasks.entry, 483 | tasks.modified, 484 | tasks.ended, 485 | tasks.status, 486 | tasks.uuid, 487 | tasks.alias 488 | FROM public.tasks 489 | WHERE ((tasks.status = 'pending'::public.task_status) AND ((tasks.scheduled < (CURRENT_DATE + '24:00:00'::interval)) OR (tasks.due < (CURRENT_DATE + '24:00:00'::interval)))) 490 | ORDER BY tasks.scheduled, tasks.due; 491 | 492 | 493 | -- 494 | -- Name: tdt_report; Type: VIEW; Schema: public; Owner: - 495 | -- 496 | 497 | CREATE VIEW public.tdt_report AS 498 | SELECT concat((('['::text || tdt.project) || '] '::text), tdt.description) AS title, 499 | concat((('SCHED '::text || to_char(tdt.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdt.due, 'MM-DD HH24:MI'::text))) AS sched, 500 | tdt.annotation AS annot, 501 | tdt.uuid 502 | FROM public.tdt; 503 | 504 | 505 | -- 506 | -- Name: tdtnw; Type: VIEW; Schema: public; Owner: - 507 | -- 508 | 509 | CREATE VIEW public.tdtnw AS 510 | SELECT tdt.scheduled, 511 | tdt.description, 512 | tdt.annotation, 513 | tdt.project, 514 | tdt.priority, 515 | tdt.due, 516 | tdt.duration, 517 | tdt.tags, 518 | tdt.parent, 519 | tdt.dependencies, 520 | tdt.entry, 521 | tdt.modified, 522 | tdt.ended, 523 | tdt.status, 524 | tdt.uuid, 525 | tdt.alias 526 | FROM public.tdt 527 | WHERE ((tdt.project IS NULL) OR (tdt.project <> 'undo'::text)); 528 | 529 | 530 | -- 531 | -- Name: tdtnw_report; Type: VIEW; Schema: public; Owner: - 532 | -- 533 | 534 | CREATE VIEW public.tdtnw_report AS 535 | SELECT tdtnw.description AS title, 536 | concat((('SCHED '::text || to_char(tdtnw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtnw.due, 'MM-DD HH24:MI'::text))) AS sched, 537 | tdtnw.annotation AS annot, 538 | tdtnw.uuid 539 | FROM public.tdtnw; 540 | 541 | 542 | -- 543 | -- Name: tdtw; Type: VIEW; Schema: public; Owner: - 544 | -- 545 | 546 | CREATE VIEW public.tdtw AS 547 | SELECT tdt.scheduled, 548 | tdt.description, 549 | tdt.annotation, 550 | tdt.project, 551 | tdt.priority, 552 | tdt.due, 553 | tdt.duration, 554 | tdt.tags, 555 | tdt.parent, 556 | tdt.dependencies, 557 | tdt.entry, 558 | tdt.modified, 559 | tdt.ended, 560 | tdt.status, 561 | tdt.uuid, 562 | tdt.alias 563 | FROM public.tdt 564 | WHERE (tdt.project = 'undo'::text); 565 | 566 | 567 | -- 568 | -- Name: tdtw_report; Type: VIEW; Schema: public; Owner: - 569 | -- 570 | 571 | CREATE VIEW public.tdtw_report AS 572 | SELECT tdtw.description AS title, 573 | concat((('SCHED '::text || to_char(tdtw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtw.due, 'MM-DD HH24:MI'::text))) AS sched, 574 | tdtw.annotation AS annot, 575 | tdtw.uuid 576 | FROM public.tdtw; 577 | 578 | 579 | -- 580 | -- Name: tasks alias_unique; Type: CONSTRAINT; Schema: public; Owner: - 581 | -- 582 | 583 | ALTER TABLE ONLY public.tasks 584 | ADD CONSTRAINT alias_unique UNIQUE (alias); 585 | 586 | 587 | -- 588 | -- Name: tasks no_recur_dup; Type: CONSTRAINT; Schema: public; Owner: - 589 | -- 590 | 591 | ALTER TABLE ONLY public.tasks 592 | ADD CONSTRAINT no_recur_dup UNIQUE (parent, due, scheduled, description); 593 | 594 | 595 | -- 596 | -- Name: tasks tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: - 597 | -- 598 | 599 | ALTER TABLE ONLY public.tasks 600 | ADD CONSTRAINT tasks_pkey PRIMARY KEY (uuid); 601 | 602 | 603 | -- 604 | -- Name: tasks changes_notify_trigger; Type: TRIGGER; Schema: public; Owner: - 605 | -- 606 | 607 | CREATE TRIGGER changes_notify_trigger AFTER INSERT OR DELETE OR UPDATE ON public.tasks FOR EACH STATEMENT EXECUTE PROCEDURE public.changes_notify_fn(); 608 | 609 | 610 | -- 611 | -- Name: tasks update_ended_trigger; Type: TRIGGER; Schema: public; Owner: - 612 | -- 613 | 614 | CREATE TRIGGER update_ended_trigger BEFORE INSERT OR UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE public.update_ended_fn(); 615 | 616 | 617 | -- 618 | -- Name: tasks update_modified_trigger; Type: TRIGGER; Schema: public; Owner: - 619 | -- 620 | 621 | CREATE TRIGGER update_modified_trigger BEFORE INSERT OR UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE public.update_modified_fn(); 622 | 623 | 624 | -- 625 | -- Name: tasks tasks_parent_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 626 | -- 627 | 628 | ALTER TABLE ONLY public.tasks 629 | ADD CONSTRAINT tasks_parent_fkey FOREIGN KEY (parent) REFERENCES public.tasks(uuid) DEFERRABLE INITIALLY DEFERRED; 630 | 631 | 632 | -- 633 | -- Name: TABLE tasks; Type: ACL; Schema: public; Owner: - 634 | -- 635 | 636 | GRANT SELECT ON TABLE public.tasks TO taskdb_grafana; 637 | 638 | 639 | -- 640 | -- Name: TABLE conflicting; Type: ACL; Schema: public; Owner: - 641 | -- 642 | 643 | GRANT SELECT ON TABLE public.conflicting TO taskdb_grafana; 644 | 645 | 646 | -- 647 | -- Name: TABLE megatasks; Type: ACL; Schema: public; Owner: - 648 | -- 649 | 650 | GRANT SELECT ON TABLE public.megatasks TO taskdb_grafana; 651 | 652 | 653 | -- 654 | -- Name: TABLE overdue; Type: ACL; Schema: public; Owner: - 655 | -- 656 | 657 | GRANT SELECT ON TABLE public.overdue TO taskdb_grafana; 658 | 659 | 660 | -- 661 | -- Name: TABLE tdt; Type: ACL; Schema: public; Owner: - 662 | -- 663 | 664 | GRANT SELECT ON TABLE public.tdt TO taskdb_grafana; 665 | 666 | 667 | -- 668 | -- Name: TABLE tdt_report; Type: ACL; Schema: public; Owner: - 669 | -- 670 | 671 | GRANT SELECT ON TABLE public.tdt_report TO taskdb_grafana; 672 | 673 | 674 | -- 675 | -- Name: TABLE tdtnw; Type: ACL; Schema: public; Owner: - 676 | -- 677 | 678 | GRANT SELECT ON TABLE public.tdtnw TO taskdb_grafana; 679 | 680 | 681 | -- 682 | -- Name: TABLE tdtnw_report; Type: ACL; Schema: public; Owner: - 683 | -- 684 | 685 | GRANT SELECT ON TABLE public.tdtnw_report TO taskdb_grafana; 686 | 687 | 688 | -- 689 | -- Name: TABLE tdtw; Type: ACL; Schema: public; Owner: - 690 | -- 691 | 692 | GRANT SELECT ON TABLE public.tdtw TO taskdb_grafana; 693 | 694 | 695 | -- 696 | -- Name: TABLE tdtw_report; Type: ACL; Schema: public; Owner: - 697 | -- 698 | 699 | GRANT SELECT ON TABLE public.tdtw_report TO taskdb_grafana; 700 | 701 | 702 | -- 703 | -- PostgreSQL database dump complete 704 | -- 705 | 706 | -------------------------------------------------------------------------------- /share/schema_changes/20181001-reorder_drop_cols.sql: -------------------------------------------------------------------------------- 1 | -- Run these one by one. 2 | 3 | CREATE TABLE tasks_v2 ( 4 | scheduled timestamp with time zone, 5 | description text, 6 | annotation text, 7 | project text, 8 | 9 | priority character varying(1), 10 | due timestamp with time zone, 11 | duration text, 12 | tags text, 13 | 14 | parent uuid, 15 | dependencies text, 16 | entry timestamp with time zone DEFAULT CURRENT_TIMESTAMP, 17 | modified timestamp with time zone DEFAULT CURRENT_TIMESTAMP, 18 | ended timestamp with time zone, 19 | 20 | status public.task_status DEFAULT 'pending'::public.task_status NOT NULL, 21 | uuid uuid DEFAULT public.uuid_generate_v4() NOT NULL 22 | 23 | -- these are being dropped 24 | -- started timestamp with time zone, 25 | -- recur text, 26 | ); 27 | 28 | 29 | INSERT INTO tasks_v2 ( 30 | scheduled, 31 | description, 32 | annotation, 33 | project, 34 | priority, 35 | due, 36 | duration, 37 | tags, 38 | parent, 39 | dependencies, 40 | entry, 41 | modified, 42 | ended, 43 | status, 44 | uuid 45 | ) SELECT 46 | scheduled, 47 | description, 48 | annotation, 49 | project, 50 | priority, 51 | due, 52 | duration, 53 | tags, 54 | parent, 55 | dependencies, 56 | entry, 57 | modified, 58 | ended, 59 | status, 60 | uuid 61 | FROM tasks; 62 | 63 | -- renames 64 | alter table tasks rename to tasks_old_schema_20181001; 65 | alter table tasks_v2 rename to tasks; 66 | 67 | -- constraints and triggers track the original table across renames. 68 | -- drop and recreate them. 69 | 70 | ALTER TABLE ONLY public.tasks_old_schema_20181001 rename constraint tasks_pkey to tasks_pkey_old_schema_20181001; 71 | ALTER TABLE ONLY public.tasks 72 | ADD CONSTRAINT tasks_pkey PRIMARY KEY (uuid); 73 | 74 | ALTER TABLE ONLY public.tasks_old_schema_20181001 rename constraint no_recur_dup to no_recur_dup_old_schema_20181001; 75 | ALTER TABLE ONLY public.tasks 76 | ADD CONSTRAINT no_recur_dup UNIQUE (parent, due, scheduled, description); 77 | 78 | ALTER TABLE ONLY public.tasks_old_schema_20181001 rename constraint tasks_parent_fkey to tasks_parent_fkey_old_schema_20181001; 79 | ALTER TABLE ONLY public.tasks 80 | ADD CONSTRAINT tasks_parent_fkey FOREIGN KEY (parent) REFERENCES public.tasks(uuid) DEFERRABLE INITIALLY DEFERRED; 81 | 82 | drop trigger changes_notify_trigger on tasks_old_schema_20181001; 83 | CREATE TRIGGER changes_notify_trigger AFTER INSERT OR DELETE OR UPDATE ON public.tasks FOR EACH STATEMENT EXECUTE PROCEDURE public.changes_notify_fn(); 84 | 85 | drop trigger update_ended_trigger on tasks_old_schema_20181001; 86 | CREATE TRIGGER update_ended_trigger BEFORE INSERT OR UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE public.update_ended_fn(); 87 | 88 | drop trigger update_modified_trigger on tasks_old_schema_20181001; 89 | CREATE TRIGGER update_modified_trigger BEFORE INSERT OR UPDATE ON public.tasks FOR EACH ROW EXECUTE PROCEDURE public.update_modified_fn(); 90 | 91 | drop view tdtw_report; 92 | drop view tdtw; 93 | drop view tdt_report; 94 | drop view tdt; 95 | 96 | CREATE VIEW public.tdt AS 97 | SELECT * 98 | FROM public.tasks 99 | WHERE ((tasks.status = 'pending'::public.task_status) AND ((tasks.scheduled < (CURRENT_DATE + '24:00:00'::interval)) OR (tasks.due < (CURRENT_DATE + '24:00:00'::interval)))) 100 | ORDER BY tasks.scheduled, tasks.due; 101 | 102 | 103 | -- 104 | -- Name: tdt_report; Type: VIEW; Schema: public; Owner: - 105 | -- 106 | 107 | CREATE VIEW public.tdt_report AS 108 | SELECT concat((('['::text || tdt.project) || '] '::text), tdt.description) AS title, 109 | concat((('SCHED '::text || to_char(tdt.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdt.due, 'MM-DD HH24:MI'::text))) AS sched, 110 | tdt.annotation AS annot, 111 | tdt.uuid 112 | FROM public.tdt; 113 | 114 | 115 | -- 116 | -- Name: tdtw; Type: VIEW; Schema: public; Owner: - 117 | -- 118 | 119 | CREATE VIEW public.tdtw AS 120 | SELECT * 121 | FROM public.tdt 122 | WHERE (tdt.project = 'undo'::text); 123 | 124 | 125 | -- 126 | -- Name: tdtw_report; Type: VIEW; Schema: public; Owner: - 127 | -- 128 | 129 | CREATE VIEW public.tdtw_report AS 130 | SELECT tdtw.description AS title, 131 | concat((('SCHED '::text || to_char(tdtw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtw.due, 'MM-DD HH24:MI'::text))) AS sched, 132 | tdtw.annotation AS annot, 133 | tdtw.uuid 134 | FROM public.tdtw; 135 | 136 | 137 | 138 | drop table tasks_old_schema_20181001; 139 | -------------------------------------------------------------------------------- /share/schema_changes/20181004-change-tdt.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW public.tdt AS 2 | SELECT tasks.scheduled, 3 | tasks.description, 4 | tasks.annotation, 5 | tasks.project, 6 | tasks.priority, 7 | tasks.due, 8 | tasks.duration, 9 | tasks.tags, 10 | tasks.parent, 11 | tasks.dependencies, 12 | tasks.entry, 13 | tasks.modified, 14 | tasks.ended, 15 | tasks.status, 16 | tasks.uuid 17 | FROM tasks 18 | WHERE ((tasks.status = 'pending'::task_status) AND ((tasks.scheduled < (CURRENT_TIMESTAMP + '24:00:00'::interval)) OR (tasks.due < (CURRENT_TIMESTAMP + '24:00:00'::interval)))) 19 | ORDER BY tasks.scheduled, tasks.due; 20 | -------------------------------------------------------------------------------- /share/schema_changes/20190201-revert-change-tdt.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW public.tdt AS 2 | SELECT tasks.scheduled, 3 | tasks.description, 4 | tasks.annotation, 5 | tasks.project, 6 | tasks.priority, 7 | tasks.due, 8 | tasks.duration, 9 | tasks.tags, 10 | tasks.parent, 11 | tasks.dependencies, 12 | tasks.entry, 13 | tasks.modified, 14 | tasks.ended, 15 | tasks.status, 16 | tasks.uuid 17 | FROM tasks 18 | WHERE ((tasks.status = 'pending'::task_status) AND ((tasks.scheduled < (CURRENT_DATE + '24:00:00'::interval)) OR (tasks.due < (CURRENT_DATE + '24:00:00'::interval)))) 19 | ORDER BY tasks.scheduled, tasks.due; 20 | -------------------------------------------------------------------------------- /share/schema_changes/20190202-duration-integer.sql: -------------------------------------------------------------------------------- 1 | \set ON_ERROR_STOP 2 | 3 | DROP VIEW tdtw_report; 4 | DROP VIEW tdtw; 5 | DROP VIEW tdt_report; 6 | DROP VIEW tdt; 7 | 8 | UPDATE tasks SET duration = NULL WHERE duration = ''; 9 | ALTER TABLE tasks ALTER duration TYPE integer USING duration::integer; 10 | 11 | CREATE VIEW public.tdt AS 12 | SELECT tasks.scheduled, 13 | tasks.description, 14 | tasks.annotation, 15 | tasks.project, 16 | tasks.priority, 17 | tasks.due, 18 | tasks.duration, 19 | tasks.tags, 20 | tasks.parent, 21 | tasks.dependencies, 22 | tasks.entry, 23 | tasks.modified, 24 | tasks.ended, 25 | tasks.status, 26 | tasks.uuid 27 | FROM public.tasks 28 | WHERE ((tasks.status = 'pending'::public.task_status) AND ((tasks.scheduled < (CURRENT_DATE + '24:00:00'::interval)) OR (tasks.due < (CURRENT_DATE + '24:00:00'::interval)))) 29 | ORDER BY tasks.scheduled, tasks.due; 30 | 31 | CREATE VIEW public.tdt_report AS 32 | SELECT concat((('['::text || tdt.project) || '] '::text), tdt.description) AS title, 33 | concat((('SCHED '::text || to_char(tdt.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdt.due, 'MM-DD HH24:MI'::text))) AS sched, 34 | tdt.annotation AS annot, 35 | tdt.uuid 36 | FROM public.tdt; 37 | 38 | CREATE VIEW public.tdtw AS 39 | SELECT tdt.scheduled, 40 | tdt.description, 41 | tdt.annotation, 42 | tdt.project, 43 | tdt.priority, 44 | tdt.due, 45 | tdt.duration, 46 | tdt.tags, 47 | tdt.parent, 48 | tdt.dependencies, 49 | tdt.entry, 50 | tdt.modified, 51 | tdt.ended, 52 | tdt.status, 53 | tdt.uuid 54 | FROM public.tdt 55 | WHERE (tdt.project = 'undo'::text); 56 | 57 | CREATE VIEW public.tdtw_report AS 58 | SELECT tdtw.description AS title, 59 | concat((('SCHED '::text || to_char(tdtw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtw.due, 'MM-DD HH24:MI'::text))) AS sched, 60 | tdtw.annotation AS annot, 61 | tdtw.uuid 62 | FROM public.tdtw; 63 | -------------------------------------------------------------------------------- /share/schema_changes/20190218-add-tdtnw.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Name: tdtnw; Type: VIEW; Schema: public; Owner: - 3 | -- 4 | 5 | CREATE VIEW public.tdtnw AS 6 | SELECT tdt.scheduled, 7 | tdt.description, 8 | tdt.annotation, 9 | tdt.project, 10 | tdt.priority, 11 | tdt.due, 12 | tdt.duration, 13 | tdt.tags, 14 | tdt.parent, 15 | tdt.dependencies, 16 | tdt.entry, 17 | tdt.modified, 18 | tdt.ended, 19 | tdt.status, 20 | tdt.uuid 21 | FROM public.tdt 22 | WHERE ((tdt.project IS NULL) OR (tdt.project <> 'undo'::text)); 23 | 24 | 25 | -- 26 | -- Name: tdtnw_report; Type: VIEW; Schema: public; Owner: - 27 | -- 28 | 29 | CREATE VIEW public.tdtnw_report AS 30 | SELECT tdtnw.description AS title, 31 | concat((('SCHED '::text || to_char(tdtnw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtnw.due, 'MM-DD HH24:MI'::text))) AS sched, 32 | tdtnw.annotation AS annot, 33 | tdtnw.uuid 34 | FROM public.tdtnw; 35 | -------------------------------------------------------------------------------- /share/schema_changes/20190404-add-megatasks-view.sql: -------------------------------------------------------------------------------- 1 | CREATE VIEW megatasks AS 2 | SELECT * FROM tasks as megatask 3 | WHERE status = 'pending' 4 | AND scheduled IS NOT NULL 5 | AND parent IS NULL 6 | AND ( 7 | -- has child tasks 8 | EXISTS(SELECT * FROM tasks AS child WHERE child.parent = megatask.uuid) 9 | OR ( 10 | -- has deps and is not a dep of anyone 11 | dependencies IS NOT NULL 12 | AND 13 | NOT EXISTS( 14 | SELECT * FROM tasks AS rdep 15 | WHERE megatask.uuid::text = ANY(string_to_array(rdep.dependencies, E'\n')) 16 | ) 17 | ) 18 | ) 19 | ORDER BY scheduled 20 | -------------------------------------------------------------------------------- /share/schema_changes/20190405-add-overdue-view.sql: -------------------------------------------------------------------------------- 1 | CREATE VIEW overdue AS 2 | SELECT * FROM tasks 3 | WHERE 4 | status = 'pending' 5 | AND ( 6 | scheduled + (COALESCE(duration, 0) * '1 second'::interval) < CURRENT_TIMESTAMP 7 | OR 8 | due < CURRENT_TIMESTAMP 9 | ) 10 | ORDER BY scheduled 11 | -------------------------------------------------------------------------------- /share/schema_changes/20190406-add-conflicting-view.sql: -------------------------------------------------------------------------------- 1 | CREATE VIEW conflicting AS 2 | SELECT * FROM tasks t 3 | WHERE 4 | scheduled IS NOT NULL 5 | AND status = 'pending' 6 | AND duration > 0 7 | AND EXISTS( 8 | SELECT * FROM tasks AS another_task 9 | WHERE 10 | another_task.uuid != t.uuid 11 | AND another_task.status = 'pending' 12 | AND another_task.duration > 0 13 | -- (StartA < EndB) and (EndA > StartB) 14 | -- Based on https://stackoverflow.com/a/325964 15 | AND t.scheduled < another_task.scheduled + (another_task.duration * '1 second'::interval) 16 | AND t.scheduled + (t.duration * '1 second'::interval) > another_task.scheduled 17 | LIMIT 1 18 | ) 19 | ORDER BY scheduled 20 | -------------------------------------------------------------------------------- /share/schema_changes/20190501-trigger-preserve-modified-if-given.sql: -------------------------------------------------------------------------------- 1 | REPLACE FUNCTION public.update_modified_fn() RETURNS trigger 2 | LANGUAGE plpgsql 3 | AS $$ 4 | BEGIN 5 | IF (NEW.modified IS NULL) THEN 6 | NEW.modified := CURRENT_TIMESTAMP; 7 | END IF; 8 | RETURN NEW; 9 | END; 10 | $$; 11 | -------------------------------------------------------------------------------- /share/schema_changes/20190502-trigger-preserve-modified-fix.sql: -------------------------------------------------------------------------------- 1 | REPLACE FUNCTION public.update_modified_fn() RETURNS trigger 2 | LANGUAGE plpgsql 3 | AS $$ 4 | BEGIN 5 | IF (NEW.modified IS NOT DISTINCT FROM OLD.modified) THEN 6 | NEW.modified := CURRENT_TIMESTAMP; 7 | END IF; 8 | RETURN NEW; 9 | END; 10 | $$; 11 | -------------------------------------------------------------------------------- /share/schema_changes/20191001-enable-grafana-access.sql: -------------------------------------------------------------------------------- 1 | CREATE USER taskdb_grafana PASSWORD 'taskdb_grafana'; 2 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO taskdb_grafana; 3 | -------------------------------------------------------------------------------- /share/schema_changes/20191002-megatasks-view-include-unscheduled.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW megatasks AS 2 | SELECT * FROM tasks as megatask 3 | WHERE status = 'pending' 4 | AND parent IS NULL 5 | AND ( 6 | -- has child tasks 7 | EXISTS(SELECT * FROM tasks AS child WHERE child.parent = megatask.uuid) 8 | OR ( 9 | -- has deps and is not a dep of anyone 10 | dependencies IS NOT NULL 11 | AND 12 | NOT EXISTS( 13 | SELECT * FROM tasks AS rdep 14 | WHERE megatask.uuid::text = ANY(string_to_array(rdep.dependencies, E'\n')) 15 | ) 16 | ) 17 | ) 18 | ORDER BY scheduled 19 | -------------------------------------------------------------------------------- /share/schema_changes/20191003-priority-text.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW conflicting; 2 | DROP VIEW megatasks; 3 | DROP VIEW overdue; 4 | 5 | DROP VIEW tdtw_report; 6 | DROP VIEW tdtw; 7 | DROP VIEW tdtnw_report; 8 | DROP VIEW tdtnw; 9 | DROP VIEW tdt_report; 10 | DROP VIEW tdt; 11 | 12 | ALTER TABLE tasks ALTER priority TYPE text USING priority::text; 13 | 14 | \i ../views/conflicting.sql 15 | \i ../views/megatasks.sql 16 | \i ../views/overdue.sql 17 | \i ../views/tdt_all.sql 18 | 19 | -- Re-apply perms for newly created views 20 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO taskdb_grafana; 21 | -------------------------------------------------------------------------------- /share/schema_changes/2020-04-02-0001-improve-graph.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/graph_node_repr.sql 2 | \i ../functions/graph.sql 3 | -------------------------------------------------------------------------------- /share/schema_changes/2020-04-02-0002-actualize-megatasks-view.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/deps.sql 2 | \i ../functions/rdeps.sql 3 | \i ../views/megatasks.sql 4 | -------------------------------------------------------------------------------- /share/schema_changes/2020-04-02-0003-add-alias-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/alias.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/2020-04-02-0004-notify-payload-truncate.sql: -------------------------------------------------------------------------------- 1 | \i ../triggers/changes_notify_fn.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/20200229-add-alias-field.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW conflicting; 2 | DROP VIEW megatasks; 3 | DROP VIEW overdue; 4 | 5 | DROP VIEW tdtw_report; 6 | DROP VIEW tdtw; 7 | DROP VIEW tdtnw_report; 8 | DROP VIEW tdtnw; 9 | DROP VIEW tdt_report; 10 | DROP VIEW tdt; 11 | 12 | ALTER TABLE tasks ADD alias text DEFAULT NULL; 13 | ALTER TABLE tasks ADD CONSTRAINT alias_unique UNIQUE (alias); 14 | 15 | \i ../views/conflicting.sql 16 | \i ../views/megatasks.sql 17 | \i ../views/overdue.sql 18 | \i ../views/tdt_all.sql 19 | 20 | -- Re-apply perms for newly created views 21 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO taskdb_grafana; 22 | -------------------------------------------------------------------------------- /share/schema_changes/20200301-add-depgraph-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/depgraph.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/20200301-add-graph-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/graph.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/20200302-fix-graph-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/graph.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/20200303-add-depgraph_root_to_selection-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/depgraph_root_to_selection.sql 2 | -------------------------------------------------------------------------------- /share/schema_changes/20200303-fix-graph-fn.sql: -------------------------------------------------------------------------------- 1 | \i ../functions/graph.sql 2 | -------------------------------------------------------------------------------- /share/snippets/tree_view.sql: -------------------------------------------------------------------------------- 1 | WITH RECURSIVE task_tree (uuid, rank) AS 2 | ( 3 | ( 4 | SELECT tasks.uuid, 1 as rank, tasks.description, tasks.scheduled, tasks.duration 5 | FROM tasks 6 | WHERE uuid = 'f4bbff9c-8899-4ae2-bd51-7f044885384d' 7 | ORDER BY scheduled 8 | ) 9 | UNION ALL 10 | ( 11 | SELECT tasks.uuid, (task_tree.rank + 1) as rank, tasks.description, tasks.scheduled, tasks.duration 12 | FROM tasks, task_tree 13 | WHERE tasks.parent = task_tree.uuid 14 | AND tasks.status = 'pending' 15 | ORDER BY scheduled 16 | ) 17 | ) 18 | SELECT REPEAT('*', rank) as rk, description, to_char(scheduled, 'MM-DD HH24:MI'::text) as scheduled, duration, uuid 19 | FROM task_tree 20 | -- JOINing at this point messes up rows ordering, avoiding it. 21 | -------------------------------------------------------------------------------- /share/triggers/changes_notify_fn.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION public.changes_notify_fn() RETURNS trigger 2 | LANGUAGE plpgsql 3 | AS $$ 4 | BEGIN 5 | PERFORM pg_notify('CHANGES', substring(replace(current_query(), E'\n', '\n') FROM 0 FOR 8000)); 6 | RETURN NEW; 7 | END; 8 | $$; 9 | 10 | -------------------------------------------------------------------------------- /share/views/conflicting.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW conflicting AS 2 | SELECT * FROM tasks t 3 | WHERE 4 | scheduled IS NOT NULL 5 | AND status = 'pending' 6 | AND duration > 0 7 | AND EXISTS( 8 | SELECT * FROM tasks AS another_task 9 | WHERE 10 | another_task.uuid != t.uuid 11 | AND another_task.status = 'pending' 12 | AND another_task.duration > 0 13 | -- (StartA < EndB) and (EndA > StartB) 14 | -- Based on https://stackoverflow.com/a/325964 15 | AND t.scheduled < another_task.scheduled + (another_task.duration * '1 second'::interval) 16 | AND t.scheduled + (t.duration * '1 second'::interval) > another_task.scheduled 17 | LIMIT 1 18 | ) 19 | ORDER BY scheduled 20 | -------------------------------------------------------------------------------- /share/views/megatasks.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW megatasks AS 2 | SELECT * FROM tasks AS megatask 3 | WHERE 4 | status = 'pending' 5 | AND megatask.scheduled IS NOT NULL 6 | AND COALESCE(megatask.duration, 0) = 0 7 | AND EXISTS ((SELECT deps(megatask.uuid))) 8 | AND NOT EXISTS 9 | (( 10 | SELECT * FROM rdeps(megatask.uuid) AS parent 11 | WHERE parent.scheduled IS NOT NULL 12 | )) 13 | ORDER BY scheduled 14 | -------------------------------------------------------------------------------- /share/views/overdue.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW overdue AS 2 | SELECT * FROM tasks 3 | WHERE 4 | status = 'pending' 5 | AND ( 6 | scheduled + (COALESCE(duration, 0) * '1 second'::interval) < CURRENT_TIMESTAMP 7 | OR 8 | due < CURRENT_TIMESTAMP 9 | ) 10 | ORDER BY scheduled 11 | -------------------------------------------------------------------------------- /share/views/tdt_all.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW public.tdt AS 2 | SELECT * 3 | FROM public.tasks 4 | WHERE ((tasks.status = 'pending'::public.task_status) AND ((tasks.scheduled < (CURRENT_DATE + '24:00:00'::interval)) OR (tasks.due < (CURRENT_DATE + '24:00:00'::interval)))) 5 | ORDER BY tasks.scheduled, tasks.due; 6 | 7 | 8 | CREATE OR REPLACE VIEW public.tdt_report AS 9 | SELECT concat((('['::text || tdt.project) || '] '::text), tdt.description) AS title, 10 | concat((('SCHED '::text || to_char(tdt.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdt.due, 'MM-DD HH24:MI'::text))) AS sched, 11 | tdt.annotation AS annot, 12 | tdt.uuid 13 | FROM public.tdt; 14 | 15 | 16 | CREATE OR REPLACE VIEW public.tdtnw AS 17 | SELECT * 18 | FROM public.tdt 19 | WHERE ((tdt.project IS NULL) OR (tdt.project <> 'undo'::text)); 20 | 21 | 22 | CREATE OR REPLACE VIEW public.tdtnw_report AS 23 | SELECT tdtnw.description AS title, 24 | concat((('SCHED '::text || to_char(tdtnw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtnw.due, 'MM-DD HH24:MI'::text))) AS sched, 25 | tdtnw.annotation AS annot, 26 | tdtnw.uuid 27 | FROM public.tdtnw; 28 | 29 | 30 | CREATE OR REPLACE VIEW public.tdtw AS 31 | SELECT * 32 | FROM public.tdt 33 | WHERE (tdt.project = 'undo'::text); 34 | 35 | 36 | CREATE OR REPLACE VIEW public.tdtw_report AS 37 | SELECT tdtw.description AS title, 38 | concat((('SCHED '::text || to_char(tdtw.scheduled, 'MM-DD HH24:MI'::text)) || ' '::text), ('DUE '::text || to_char(tdtw.due, 'MM-DD HH24:MI'::text))) AS sched, 39 | tdtw.annotation AS annot, 40 | tdtw.uuid 41 | FROM public.tdtw; 42 | --------------------------------------------------------------------------------