├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py └── index.rst ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── models.py └── tests.py ├── tox.ini └── workflow_activity ├── __init__.py ├── admin.py ├── managers.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .coveralls.yml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: "2.7" 3 | env: 4 | - TOX_ENV=py27-dj17 5 | - TOX_ENV=py34-dj17 6 | - TOX_ENV=py27-dj18 7 | - TOX_ENV=py34-dj18 8 | install: 9 | install: 10 | install: 11 | - pip install tox --use-mirrors 12 | script: tox -e $TOX_ENV 13 | after_success: 14 | - pip install coveralls 15 | - coveralls 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-workflow-activity 2 | ======================== 3 | .. image:: https://travis-ci.org/unistra/django-workflow-activity.svg?branch=master 4 | :target: https://travis-ci.org/unistra/django-workflow-activity 5 | 6 | .. image:: https://coveralls.io/repos/unistra/django-workflow-activity/badge.svg?branch=master 7 | :target: https://coveralls.io/r/unistra/django-workflow-activity?branch=master 8 | 9 | Install 10 | ------- 11 | 12 | Install the package via pypi: :: 13 | 14 | pip install django-workflow-activity 15 | 16 | Add the installed application in the django settings file: :: 17 | 18 | INSTALLED_APPS = ( 19 | ... 20 | 'workflow_activity' 21 | ) 22 | 23 | Migrate the database: :: 24 | 25 | python manage.py migrate 26 | 27 | Usage 28 | ----- 29 | 30 | To create workflows and permissions, see the following documentations: 31 | 32 | - https://pythonhosted.org/django-workflows 33 | - https://pythonhosted.org/django-permissions 34 | 35 | To use workflow activity methods on a class : :: 36 | 37 | from workflow_activity.models import WorkflowManagedInstance 38 | 39 | class MyClass(WorkflowManagedInstance): 40 | ... 41 | 42 | To add a workflow to an object: :: 43 | 44 | myobj = MyClass() 45 | myobj.set_workflow('My workflow') 46 | 47 | Now, you can use methods on your object like: :: 48 | 49 | myobj.last_state() 50 | myobj.last_transition() 51 | myobj.last_actor() 52 | myobj.last_action() 53 | myobj.allowed_transitions(request.user) 54 | myobj.is_editable_by(request.user, permission='edit') 55 | myobj.state() 56 | myobj.change_state(transition, request.user) 57 | ... 58 | 59 | And managers like: :: 60 | 61 | MyClass.objects.filter() 62 | MyClass.pending.filter() 63 | MyClass.ended.filter() 64 | ... 65 | 66 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-workflow-activity.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-workflow-activity.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-workflow-activity" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-workflow-activity" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-workflow-activity documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Jun 18 12:15:17 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.viewcode', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'django-workflow-activity' 56 | copyright = '2015, Arnaud Grausem' 57 | author = 'Arnaud Grausem' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '1.0' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '1.0' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ['_build'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = True 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | html_theme = 'alabaster' 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | #html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | #html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (within the static path) to use as favicon of the 139 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | #html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | #html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | #html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | #html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | #html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | #html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | #html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | #html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | #html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 198 | #html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'django-workflow-activitydoc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'django-workflow-activity.tex', 'django-workflow-activity Documentation', 232 | 'Arnaud Grausem', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'django-workflow-activity', 'django-workflow-activity Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'django-workflow-activity', 'django-workflow-activity Documentation', 276 | author, 'django-workflow-activity', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | 292 | 293 | # -- Options for Epub output ---------------------------------------------- 294 | 295 | # Bibliographic Dublin Core info. 296 | epub_title = project 297 | epub_author = author 298 | epub_publisher = author 299 | epub_copyright = copyright 300 | 301 | # The basename for the epub file. It defaults to the project name. 302 | #epub_basename = project 303 | 304 | # The HTML theme for the epub output. Since the default themes are not optimized 305 | # for small screen space, using the same theme for HTML and epub output is 306 | # usually not wise. This defaults to 'epub', a theme designed to save visual 307 | # space. 308 | #epub_theme = 'epub' 309 | 310 | # The language of the text. It defaults to the language option 311 | # or 'en' if the language is not set. 312 | #epub_language = '' 313 | 314 | # The scheme of the identifier. Typical schemes are ISBN or URL. 315 | #epub_scheme = '' 316 | 317 | # The unique identifier of the text. This can be a ISBN number 318 | # or the project homepage. 319 | #epub_identifier = '' 320 | 321 | # A unique identification for the text. 322 | #epub_uid = '' 323 | 324 | # A tuple containing the cover image and cover page html template filenames. 325 | #epub_cover = () 326 | 327 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 328 | #epub_guide = () 329 | 330 | # HTML files that should be inserted before the pages created by sphinx. 331 | # The format is a list of tuples containing the path and title. 332 | #epub_pre_files = [] 333 | 334 | # HTML files shat should be inserted after the pages created by sphinx. 335 | # The format is a list of tuples containing the path and title. 336 | #epub_post_files = [] 337 | 338 | # A list of files that should not be packed into the epub file. 339 | epub_exclude_files = ['search.html'] 340 | 341 | # The depth of the table of contents in toc.ncx. 342 | #epub_tocdepth = 3 343 | 344 | # Allow duplicate toc entries. 345 | #epub_tocdup = True 346 | 347 | # Choose between 'default' and 'includehidden'. 348 | #epub_tocscope = 'default' 349 | 350 | # Fix unsupported image types using the Pillow. 351 | #epub_fix_images = False 352 | 353 | # Scale large images. 354 | #epub_max_image_width = 0 355 | 356 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 357 | #epub_show_urls = 'inline' 358 | 359 | # If false, no index is generated. 360 | #epub_use_index = True 361 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-workflow-activity documentation master file, created by 2 | sphinx-quickstart on Thu Jun 18 12:15:17 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-workflow-activity's documentation! 7 | ==================================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | .. automodule:: workflow_activity.models 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-workflows-unistra 2 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.conf import settings 3 | from django.core.management import execute_from_command_line 4 | 5 | if not settings.configured: 6 | settings.configure( 7 | DATABASES={ 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': ':memory:', 11 | }, 12 | }, 13 | INSTALLED_APPS=( 14 | 'django.contrib.auth', 15 | 'django.contrib.contenttypes', 16 | 'django.contrib.sessions', 17 | 'django.contrib.sites', 18 | 'django.contrib.messages', 19 | 'django.contrib.staticfiles', 20 | 'django.contrib.flatpages', 21 | 'workflows', 22 | 'permissions', 23 | 'workflow_activity', 24 | 'tests' 25 | ), 26 | MIDDLEWARE_CLASSES = ( 27 | 'django.middleware.common.CommonMiddleware', 28 | 'django.contrib.sessions.middleware.SessionMiddleware', 29 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 30 | ), 31 | ROOT_URLCONF=None, 32 | USE_TZ=True, 33 | SECRET_KEY='foobar', 34 | # SILENCED_SYSTEM_CHECKS=['1_7.W001'], 35 | ) 36 | 37 | 38 | def runtests(): 39 | argv = sys.argv[:1] + ['test'] + sys.argv[1:] 40 | execute_from_command_line(argv) 41 | 42 | if __name__ == '__main__': 43 | runtests() 44 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from setuptools import setup, find_packages 6 | 7 | 8 | def recursive_requirements(requirement_file, libs, links, path=''): 9 | if not requirement_file.startswith(path): 10 | requirement_file = os.path.join(path, requirement_file) 11 | with open(requirement_file) as requirements: 12 | for requirement in requirements.readlines(): 13 | if requirement.startswith('-r'): 14 | requirement_file = requirement.split()[1] 15 | if not path: 16 | path = requirement_file.rsplit('/', 1)[0] 17 | recursive_requirements(requirement_file, libs, links, 18 | path=path) 19 | elif requirement.startswith('-f'): 20 | links.append(requirement.split()[1]) 21 | else: 22 | libs.append(requirement) 23 | 24 | 25 | with open('README.rst') as readme: 26 | long_description = readme.read() 27 | 28 | 29 | libraries, dependency_links = [], [] 30 | recursive_requirements('requirements.txt', libraries, dependency_links) 31 | 32 | 33 | setup( 34 | name = "django-workflow-activity", 35 | version = "1.2.0", 36 | packages = find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), 37 | 38 | install_requires=libraries, 39 | dependency_links=dependency_links, 40 | include_package_data=True, 41 | long_description=long_description, 42 | author = 'Arnaud Grausem', 43 | author_email = 'arnaud.grausem@unistra.fr', 44 | description = 'Manage all events on workflows', 45 | keywords = "workflows django events log", 46 | url = 'https://github.com/unistra/django-workflow-activity', 47 | 48 | classifiers = ['Development Status :: 5 - Production/Stable', 49 | 'Environment :: Web Environment', 50 | 'Framework :: Django', 51 | 'Intended Audience :: Developers', 52 | 'Natural Language :: English', 53 | 'Operating System :: POSIX :: Linux', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 56 | ], 57 | 58 | ) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unistra/django-workflow-activity/875518e64573e7da3640cda8dae5ca5c86f0f705/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from workflow_activity.models import WorkflowManagedInstance 3 | 4 | 5 | class FlatPage(WorkflowManagedInstance): 6 | 7 | url = models.CharField('URL', max_length=100, db_index=True) 8 | title = models.CharField('title', max_length=200) 9 | content = models.TextField('content', blank=True) 10 | 11 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | """ 5 | 6 | from django.db.models.query import QuerySet 7 | from django.contrib.auth.models import User 8 | from django.test import TestCase 9 | import permissions 10 | from workflows.tests import create_workflow 11 | from workflows.utils import set_workflow 12 | from workflows.models import State 13 | from workflows.models import StatePermissionRelation 14 | from workflows.models import Transition 15 | from workflows.models import Workflow 16 | from workflows.models import WorkflowPermissionRelation 17 | 18 | from workflow_activity.models import Action 19 | from workflow_activity.models import WorkflowManagedInstance 20 | from workflow_activity.models import changed_state 21 | from workflow_activity.utils import get_ending_states 22 | 23 | from .models import FlatPage 24 | 25 | # patch FlatPage to make work inheritance with WorkflowManagedInstance 26 | 27 | 28 | class ActionTest(TestCase): 29 | 30 | def setUp(self): 31 | create_workflow(self) 32 | self.user = User.objects.create(username='test_user', 33 | first_name='Test', last_name='User') 34 | self.flat_page = FlatPage.objects.create(url='/page-1', title='Page 1', 35 | initializer=self.user) 36 | 37 | def test_unicode(self): 38 | """ 39 | """ 40 | action = Action.objects.create(actor=self.user, workflow=self.w, 41 | transition=self.make_public, previous_state=self.private, 42 | content_object=self.flat_page) 43 | self.assertEqual(action.__unicode__(), 44 | u'flat page #1 - Standard - Test User - Make public') 45 | 46 | 47 | class WorkflowManagedInstanceTest(TestCase): 48 | """ 49 | """ 50 | 51 | def setUp(self): 52 | """ 53 | """ 54 | create_workflow(self) 55 | 56 | # roles 57 | self.anonymous = permissions.utils.register_role('Anonymous') 58 | self.publisher = permissions.utils.register_role('Publisher') 59 | 60 | 61 | self.anonymous_user = User.objects.create(username='anonymous_user') 62 | permissions.utils.add_role(self.anonymous_user, self.anonymous) 63 | self.test_user = User.objects.create(username='test_user', 64 | first_name='Test', last_name='User') 65 | permissions.utils.add_role(self.test_user, self.publisher) 66 | 67 | self.flat_page = FlatPage.objects.create(url='/page-1', title='Page 1', 68 | initializer=self.test_user) 69 | 70 | # permissions 71 | self.edit = permissions.utils.register_permission('Edit', 'edit') 72 | self.view = permissions.utils.register_permission('View', 'view') 73 | 74 | # state, transition 75 | self.rejected = State.objects.create(name='Rejected', workflow=self.w) 76 | self.reject = Transition.objects.create(name='Reject', 77 | workflow=self.w, destination=self.rejected, 78 | permission=self.edit) 79 | self.private.transitions.add(self.reject) 80 | 81 | # permissions for the workflow 82 | WorkflowPermissionRelation.objects.create(workflow=self.w, 83 | permission=self.edit) 84 | WorkflowPermissionRelation.objects.create(workflow=self.w, 85 | permission=self.view) 86 | 87 | # permissions for states 88 | StatePermissionRelation.objects.create(state=self.public, 89 | permission=self.view, role=self.publisher) 90 | StatePermissionRelation.objects.create(state=self.private, 91 | permission=self.edit, role=self.publisher) 92 | StatePermissionRelation.objects.create(state=self.private, 93 | permission=self.view, role=self.publisher) 94 | StatePermissionRelation.objects.create(state=self.rejected, 95 | permission=self.view, role=self.publisher) 96 | 97 | # permissions on transition 98 | self.make_public.permission = self.edit 99 | self.make_public.save() 100 | self.make_private.permission = self.edit 101 | self.make_private.save() 102 | 103 | set_workflow(self.flat_page, self.w) 104 | 105 | def tearDown(self): 106 | """ 107 | """ 108 | self.flat_page.delete() 109 | 110 | def test_manage_state(self): 111 | """ 112 | """ 113 | # testing initial state 114 | self.assertEqual(self.flat_page.state, self.private) 115 | # testing changing state 116 | self.flat_page.change_state(self.make_public, self.test_user) 117 | self.assertEqual(self.flat_page.state, self.public) 118 | 119 | def test_editable(self): 120 | """ 121 | """ 122 | # check if content object is editable and editable by different users 123 | # at initial state 124 | self.assertTrue(self.flat_page.is_editable) 125 | self.assertTrue(self.flat_page.is_editable_by(self.test_user)) 126 | self.assertFalse(self.flat_page.is_editable_by(self.anonymous_user)) 127 | 128 | # check if content object is editable and editable by different users 129 | # when changing state 130 | self.flat_page.change_state(self.make_public, self.test_user) 131 | self.assertTrue(self.flat_page.is_editable) 132 | self.assertFalse(self.flat_page.is_editable_by(self.test_user)) 133 | self.assertFalse(self.flat_page.is_editable_by(self.anonymous_user)) 134 | 135 | # changing state to an ending state 136 | self.flat_page.change_state(self.reject, self.test_user) 137 | self.assertFalse(self.flat_page.is_editable) 138 | 139 | def test_allowed_transitions(self): 140 | """ 141 | """ 142 | 143 | result = self.flat_page.allowed_transitions(self.anonymous_user) 144 | self.assertListEqual(result, []) 145 | result = self.flat_page.allowed_transitions(self.test_user) 146 | self.assertEqual(len(result), 2) 147 | 148 | self.flat_page.change_state(self.make_public, self.test_user) 149 | result = self.flat_page.allowed_transitions(self.anonymous_user) 150 | self.assertListEqual(result, []) 151 | result = self.flat_page.allowed_transitions(self.test_user) 152 | self.assertEqual(result, []) 153 | 154 | def test_allowed_transition(self): 155 | """ 156 | """ 157 | result = self.flat_page.allowed_transition(self.make_private.id, 158 | self.test_user) 159 | self.assertIsNone(result) 160 | result = self.flat_page.allowed_transition(self.make_public.id, 161 | self.test_user) 162 | self.assertEqual(result, self.make_public) 163 | 164 | self.flat_page.change_state(self.make_public, self.test_user) 165 | result = self.flat_page.allowed_transition(self.make_private.id, 166 | self.test_user) 167 | self.assertIsNone(result) 168 | 169 | def test_create_actions(self): 170 | """ 171 | """ 172 | self.flat_page.change_state(self.make_public, self.test_user) 173 | 174 | actions = self.flat_page.actions.all() 175 | self.assertEqual(actions.count(), 1) 176 | 177 | self.flat_page.change_state(self.make_private, self.test_user) 178 | self.assertEqual(actions.count(), 2) 179 | 180 | action = actions[1] 181 | self.assertEqual(action.previous_state, self.public) 182 | self.assertEqual(action.transition, self.make_private) 183 | self.assertEqual(action.actor, self.test_user) 184 | self.assertEqual(action.workflow, self.w) 185 | self.assertIsInstance(action.content_object, FlatPage) 186 | 187 | def test_last_action(self): 188 | """ 189 | """ 190 | new_action = Action.objects.create(actor=self.test_user, 191 | transition=self.make_public, previous_state=self.private, 192 | workflow=self.w, content_object=self.flat_page) 193 | 194 | self.assertEqual(self.flat_page.last_action(), new_action) 195 | 196 | def test_no_action(self): 197 | """ 198 | """ 199 | self.assertRaises(Action.DoesNotExist, self.flat_page.last_action) 200 | self.assertIsNone(self.flat_page.last_transition()) 201 | self.assertIsNone(self.flat_page.last_actor()) 202 | self.assertIsNone(self.flat_page.last_state()) 203 | 204 | def test_last_transition(self): 205 | """ 206 | """ 207 | Action.objects.create(actor=self.test_user, 208 | transition=self.make_public, previous_state=self.private, 209 | workflow=self.w, content_object=self.flat_page) 210 | self.assertEqual(self.flat_page.last_transition(), self.make_public) 211 | 212 | def test_last_actor(self): 213 | """ 214 | """ 215 | Action.objects.create(actor=self.test_user, 216 | transition=self.make_public, previous_state=self.private, 217 | workflow=self.w, content_object=self.flat_page) 218 | self.assertEqual(self.flat_page.last_actor(), self.test_user) 219 | 220 | 221 | def test_last_state(self): 222 | """ 223 | """ 224 | Action.objects.create(actor=self.test_user, 225 | transition=self.make_public, previous_state=self.private, 226 | workflow=self.w, content_object=self.flat_page) 227 | self.assertEqual(self.flat_page.last_state(), self.private) 228 | 229 | def test_get_editable_instances(self): 230 | """ 231 | """ 232 | second_page = FlatPage.objects.create(url='/page-2', title='Page 2', 233 | initializer=self.test_user) 234 | third_page = FlatPage.objects.create(url='/page-3', title='Page 3', 235 | initializer=self.test_user) 236 | fourth_page = FlatPage.objects.create(url='/page-4', title='Page 4', 237 | initializer=self.test_user) 238 | fifth_page = FlatPage.objects.create(url='/page-5', title='Page 5', 239 | initializer=self.test_user) 240 | 241 | set_workflow(self.flat_page, self.w) 242 | set_workflow(second_page, self.w) 243 | set_workflow(third_page, self.w) 244 | 245 | result = FlatPage.pending.editable_by_roles([self.publisher]) 246 | self.assertListEqual(list(result), [self.flat_page, second_page, 247 | third_page]) 248 | result = FlatPage.pending.editable_by_roles([self.anonymous]) 249 | self.assertListEqual(list(result), []) 250 | 251 | self.flat_page.change_state(self.make_public, self.test_user) 252 | second_page.change_state(self.reject, self.test_user) 253 | 254 | result = FlatPage.pending.editable_by_roles([self.publisher]) 255 | self.assertListEqual(list(result), [third_page]) 256 | 257 | 258 | class WorkflowInstanceManager(TestCase): 259 | """ 260 | """ 261 | 262 | def setUp(self): 263 | create_workflow(self) 264 | self.user = User.objects.create(username='test_user', 265 | first_name='Test', last_name='User') 266 | self.first_page = FlatPage.objects.create(url='/page-1', 267 | title='Page 1', initializer=self.user) 268 | self.second_page = FlatPage.objects.create(url='/page-2', 269 | title='Page 2', initializer=self.user) 270 | self.third_page = FlatPage.objects.create(url='/page-3', 271 | title='Page 3', initializer=self.user) 272 | self.fourth_page = FlatPage.objects.create(url='/page-4', 273 | title='Page 4', initializer=self.user) 274 | self.fifth_page = FlatPage.objects.create(url='/page-5', 275 | title='Page 5', initializer=self.user) 276 | 277 | # new transition and state 278 | self.rejected = State.objects.create(name='Rejected', workflow=self.w) 279 | self.reject = Transition.objects.create(name='Reject', 280 | workflow=self.w, destination=self.rejected) 281 | self.private.transitions.add(self.reject) 282 | 283 | def tearDown(self): 284 | self.first_page.delete() 285 | self.second_page.delete() 286 | self.third_page.delete() 287 | self.fourth_page.delete() 288 | self.fifth_page.delete() 289 | 290 | def test_by_state(self): 291 | set_workflow(self.first_page, self.w) 292 | set_workflow(self.second_page, self.w) 293 | set_workflow(self.third_page, self.w) 294 | 295 | result = FlatPage.objects.by_state('Private') 296 | self.assertListEqual(list(result), [self.first_page, self.second_page, 297 | self.third_page]) 298 | self.assertIsInstance(result, QuerySet) 299 | 300 | self.first_page.change_state(self.make_public, self.user) 301 | 302 | result = FlatPage.objects.by_state('Public') 303 | self.assertListEqual(list(result), [self.first_page]) 304 | 305 | 306 | def test_pending_manager(self): 307 | self.first_page.set_workflow(self.w.name) 308 | self.second_page.set_workflow(self.w.name) 309 | self.third_page.set_workflow(self.w.name) 310 | 311 | result = FlatPage.pending.all() 312 | self.assertListEqual(list(result), [self.first_page, self.second_page, 313 | self.third_page]) 314 | 315 | self.first_page.change_state(self.make_public, self.user) 316 | self.second_page.change_state(self.reject, self.user) 317 | result = FlatPage.pending.all() 318 | self.assertListEqual(list(result), [self.first_page, self.third_page]) 319 | 320 | result = result.by_state('Private') 321 | self.assertListEqual(list(result), [self.third_page]) 322 | 323 | def test_ended_manager(self): 324 | set_workflow(self.first_page, self.w) 325 | set_workflow(self.second_page, self.w) 326 | set_workflow(self.third_page, self.w) 327 | 328 | result = FlatPage.ended.all() 329 | self.assertListEqual(list(result), []) 330 | 331 | self.first_page.change_state(self.make_public, self.user) 332 | self.second_page.change_state(self.reject, self.user) 333 | result = FlatPage.ended.all() 334 | self.assertListEqual(list(result), [self.second_page]) 335 | 336 | result = result.by_state('Rejected') 337 | self.assertListEqual(list(result), [self.second_page]) 338 | 339 | 340 | class EndingStatesTest(TestCase): 341 | """ 342 | """ 343 | 344 | 345 | def test_get_ending_states(self): 346 | """ 347 | """ 348 | 349 | # no defined state for workflow 350 | self.w = Workflow.objects.create(name='Standard') 351 | self.assertListEqual(list(get_ending_states(self.w)), []) 352 | 353 | self.private = State.objects.create(name='Private', workflow=self.w) 354 | self.public = State.objects.create(name='Public', workflow=self.w) 355 | 356 | # two states with no transition -> ending states 357 | self.assertListEqual(list(get_ending_states(self.w)), [self.private, 358 | self.public]) 359 | 360 | self.make_public = Transition.objects.create(name='Make public', 361 | workflow=self.w, destination=self.public) 362 | self.private.transitions.add(self.make_public) 363 | 364 | # branching a transition on a state 365 | self.assertListEqual(list(get_ending_states(self.w)), [self.public]) 366 | 367 | self.make_private = Transition.objects.create(name='Make private', 368 | workflow=self.w, destination=self.private) 369 | self.public.transitions.add(self.make_private) 370 | 371 | # cycle transitions -> no ending states 372 | self.assertListEqual(list(get_ending_states(self.w)), []) 373 | 374 | 375 | class StateChangedSignalsTest(TestCase): 376 | """ 377 | """ 378 | 379 | def setUp(self): 380 | """ 381 | """ 382 | create_workflow(self) 383 | self.user = User.objects.create(username='test_user', 384 | first_name='Test', last_name='User') 385 | self.flat_page = FlatPage.objects.create(url='/page-1', title='Page 1', 386 | initializer=self.user) 387 | 388 | set_workflow(self.flat_page, self.w) 389 | 390 | def _receive_signal(self, sender, **kwargs): 391 | self.sender = sender 392 | self.signal_args = kwargs 393 | 394 | def test_changing_state(self): 395 | """ 396 | """ 397 | # connecting to the signal with a receiver function 398 | changed_state.connect(self._receive_signal, sender=self.flat_page) 399 | 400 | # changing state - signal sent 401 | self.flat_page.change_state(self.make_public, self.user) 402 | 403 | self.assertEqual(self.sender.__class__.__base__, 404 | WorkflowManagedInstance) 405 | self.assertEqual(self.signal_args['previous_state'], self.private) 406 | self.assertEqual(self.signal_args['actor'], self.user) 407 | self.assertEqual(self.signal_args['transition'], self.make_public) 408 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27-dj17,py34-dj17,py27-dj18,py34-dj18 4 | 5 | 6 | 7 | ############## 8 | # Django 1.7 # 9 | ############## 10 | 11 | [testenv:py27-dj17] 12 | basepython = python2.7 13 | deps = 14 | {[testenv]deps} 15 | django==1.7 16 | 17 | [testenv:py34-dj17] 18 | basepython = python3.4 19 | deps = {[testenv:py27-dj17]deps} 20 | 21 | [testenv:py27-dj18] 22 | basepython = python2.7 23 | deps = 24 | {[testenv]deps} 25 | django>1.8,<1.9 26 | 27 | [testenv:py34-dj18] 28 | basepython = python3.4 29 | deps = {[testenv:py27-dj18]deps} 30 | 31 | ############ 32 | # Test env # 33 | ############ 34 | 35 | [testenv] 36 | deps = 37 | coverage 38 | commands = 39 | {envpython} --version 40 | coverage run --source=workflow_activity runtests.py tests 41 | -------------------------------------------------------------------------------- /workflow_activity/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | workflow_activity 5 | ================= 6 | 7 | """ 8 | 9 | 10 | _ENDING_STATES = {} 11 | -------------------------------------------------------------------------------- /workflow_activity/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | workflow_activity.admin 5 | ======================= 6 | 7 | Enables the admin interface for the workflow_activity application. The 8 | :py:class:~arc.workflow_activity.Activity can be managed from this interface 9 | """ 10 | 11 | from django.contrib import admin 12 | from django.utils.translation import ugettext_lazy as _ 13 | 14 | from .models import Action 15 | 16 | 17 | class ActionAdmin(admin.ModelAdmin): 18 | list_display = ('content_object_display', 'actor_name', 'workflow', 19 | 'transition', 'previous_state', 'process_date') 20 | list_display_links = ('content_object_display', ) 21 | list_filter = ('workflow', 'previous_state') 22 | exclude = ('actor', ) 23 | readonly_fields = ('content_type', 'object_id', 'actor_name', 'workflow', 24 | 'transition', 'previous_state') 25 | 26 | def content_object_display(self, obj): 27 | return '{0.content_type} #{0.object_id}'.format(obj) 28 | content_object_display.short_description = _('Model instance') 29 | 30 | 31 | admin.site.register(Action, ActionAdmin) 32 | -------------------------------------------------------------------------------- /workflow_activity/managers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | workflow_activity.managers 5 | ========================== 6 | 7 | The module defines 2 managers that inherits BaseManager. They are already 8 | plugged into the WorkflowManagedInstance model and are also available in each 9 | model that inherits the WorkflowManagedInstance model. 10 | """ 11 | 12 | from django.db import models 13 | 14 | 15 | class BaseQuerySet(models.QuerySet): 16 | """ Base queryset for all workflow managed instances managers.""" 17 | 18 | def by_state(self, state_name): 19 | """ Search workflow managed instances by state 20 | 21 | :param state_name: the name of the state 22 | :type state_name: a string 23 | """ 24 | return self.filter(state_relation__state__name=state_name) 25 | 26 | 27 | class PendingQuerySet(BaseQuerySet): 28 | """ Base queryset for pending workflow managed instances managers.""" 29 | 30 | def editable_by_roles(self, roles, edit='edit'): 31 | """ Only the instances that are editable by some roles (based on 32 | permissions for this role) 33 | 34 | :param roles: a list of roles 35 | :type roles: list of `permissions.models.Role 36 | `_ 37 | :param edit: the codename of the permission to match 38 | :type edit: a string 39 | """ 40 | return self.filter( 41 | state_relation__state__statepermissionrelation__role__in=roles, 42 | state_relation__state__statepermissionrelation__permission__codename=edit 43 | ) 44 | 45 | 46 | class PendingManager(models.Manager): 47 | """ Manager that filters the instances that are currently managed by a 48 | workflow 49 | """ 50 | 51 | def get_queryset(self): 52 | """ Only the instances that are in non ending states 53 | """ 54 | return super(PendingManager, self).get_queryset()\ 55 | .filter(state_relation__state__isnull=False)\ 56 | .exclude(state_relation__state__transitions__isnull=True) 57 | 58 | 59 | class EndedManager(models.Manager): 60 | """ Manager that filters the instances that are currently in a ended state 61 | of a workflow 62 | """ 63 | 64 | def get_queryset(self): 65 | """ Only the instances that are in ending states 66 | """ 67 | return super(EndedManager, self).get_queryset()\ 68 | .filter(state_relation__state__isnull=False)\ 69 | .filter(state_relation__state__transitions__isnull=True) 70 | -------------------------------------------------------------------------------- /workflow_activity/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('contenttypes', '0001_initial'), 13 | ('workflows', '__first__'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Action', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('process_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), 22 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Date of creation')), 23 | ('object_id', models.PositiveIntegerField()), 24 | ('actor', models.ForeignKey(related_name='workflow_actions', verbose_name='Actor', to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), 25 | ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), 26 | ('previous_state', models.ForeignKey(related_name='+', verbose_name='Previous state', to='workflows.State', on_delete=models.CASCADE)), 27 | ('transition', models.ForeignKey(related_name='+', verbose_name='Transition', to='workflows.Transition', on_delete=models.CASCADE)), 28 | ('workflow', models.ForeignKey(related_name='+', verbose_name='Workflow', to='workflows.Workflow', on_delete=models.CASCADE)), 29 | ], 30 | options={ 31 | 'verbose_name': 'Action', 32 | 'verbose_name_plural': 'Actions', 33 | }, 34 | bases=(models.Model,), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /workflow_activity/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unistra/django-workflow-activity/875518e64573e7da3640cda8dae5ca5c86f0f705/workflow_activity/migrations/__init__.py -------------------------------------------------------------------------------- /workflow_activity/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | workflow_activity.models 5 | ======================== 6 | 7 | The models defined here add an history logger to all models that are processed 8 | within a workflow. :: 9 | 10 | * :py:class:`~arc.workflow_activity.Action' is the main model that stores 11 | data on each action made by a user through an application interface 12 | 13 | """ 14 | 15 | 16 | from django.contrib.contenttypes.models import ContentType 17 | from django.contrib.contenttypes.fields import GenericForeignKey 18 | from django.contrib.contenttypes.fields import GenericRelation 19 | 20 | from django.db import models, transaction 21 | from django.dispatch import receiver 22 | from django.dispatch import Signal 23 | from django.db.models.signals import m2m_changed 24 | from django.db.models.signals import post_save 25 | from django.utils.translation import ugettext_lazy as _ 26 | 27 | from permissions.utils import has_permission 28 | import workflows.models 29 | from workflows.utils import get_allowed_transitions 30 | from workflows.utils import get_state 31 | from workflows.utils import set_state 32 | from workflows.utils import set_workflow_for_object 33 | from workflows.utils import get_workflow_for_model 34 | 35 | from . import managers 36 | from .utils import get_ending_states 37 | 38 | 39 | # signals to send when the state of a workflow managed instance is changed 40 | changed_state = Signal(providing_args=['transition', 'actor', 41 | 'previous_state']) 42 | 43 | 44 | class Action(models.Model): 45 | """ This model is an history logger for actions made on a managed worklow 46 | instance. The following informations were made available : :: 47 | 48 | .. py:attribute:: actor 49 | 50 | The last actor for the processed workflow on the instance 51 | 52 | .. py:attribute:: process_date 53 | 54 | The date the action were performed by the actor 55 | 56 | .. py:attribute:: transition 57 | 58 | The transition that where called by the actor 59 | 60 | .. py:attribute:: previous_state 61 | 62 | The previous state of the managed instance, before the transition 63 | were called by the actor 64 | 65 | .. py:attribute:: workflow 66 | 67 | The workflow that were processed when transition were called by 68 | the actor 69 | 70 | .. py:attribute:: content_type 71 | 72 | The real model of the workflow managed instance 73 | 74 | .. py:attribute:: object_id 75 | 76 | The identifier of the workflow managed instance 77 | 78 | .. py:attribute:: content_object 79 | 80 | The generic foreign key between the workflow managed instance and 81 | the activity 82 | 83 | .. py:attribute:: creation_date 84 | 85 | Creation datetime of the action 86 | 87 | """ 88 | 89 | actor = models.ForeignKey('auth.User', verbose_name=_('Actor'), 90 | related_name='workflow_actions', null=True, on_delete=models.CASCADE) 91 | process_date = models.DateTimeField(verbose_name=_('Creation date'), 92 | auto_now_add=True) 93 | transition = models.ForeignKey('workflows.Transition', 94 | verbose_name=_('Transition'), related_name='+', on_delete=models.CASCADE) 95 | previous_state = models.ForeignKey('workflows.State', 96 | verbose_name=_('Previous state'), related_name='+', on_delete=models.CASCADE) 97 | workflow = models.ForeignKey('workflows.Workflow', 98 | verbose_name=_('Workflow'), related_name='+', on_delete=models.CASCADE) 99 | creation_date = models.DateTimeField(verbose_name=_('Date of creation'), 100 | auto_now_add=True) 101 | 102 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) 103 | object_id = models.PositiveIntegerField() 104 | content_object = GenericForeignKey('content_type', 'object_id') 105 | 106 | 107 | class Meta: 108 | verbose_name = _('Action') 109 | verbose_name_plural = _('Actions') 110 | app_label = 'workflow_activity' 111 | 112 | def actor_name(self): 113 | return u'{0.first_name} {0.last_name}'.format(self.actor) \ 114 | if self.actor else u'Auto' 115 | actor_name.short_description = _('Actor') 116 | actor_name = property(actor_name) 117 | 118 | def __unicode__(self): 119 | return u'{0.content_type} #{0.object_id} - {0.workflow.name} - ' \ 120 | '{0.actor_name} - {0.transition.name}'.format(self) 121 | 122 | def __str__(self): 123 | return '{0.content_type} #{0.object_id} - {0.workflow.name} - ' \ 124 | '{0.actor_name} - {0.transition.name}'.format(self) 125 | 126 | 127 | class WorkflowManagedInstance(models.Model): 128 | """ Abstract model that must be inherited by models you want to manage an 129 | history with actions, change and get states easily on instance, get edit 130 | property and permission and get allowed transitions for users. :: 131 | 132 | .. py:attribute:: actions 133 | 134 | actions on instance as a Django generic relation 135 | 136 | .. py:attribute:: state_relation 137 | 138 | relation to states on instance as a Django generic relation 139 | 140 | .. py:attribute:: initializer 141 | 142 | user who initiates the workflow on instance (can be null) 143 | 144 | .. py:attribute:: creation_date 145 | 146 | date of creation of the managed instance 147 | """ 148 | 149 | actions = GenericRelation(Action, 150 | content_type_field='content_type', 151 | object_id_field='object_id') 152 | state_relation = GenericRelation('workflows.StateObjectRelation', 153 | object_id_field='content_id') 154 | initializer = models.ForeignKey('auth.User', verbose_name=_('Initializer'), 155 | related_name='initiated_%(class)ss'.lower(), null=True, on_delete=models.CASCADE) 156 | creation_date = models.DateTimeField(verbose_name=_('Date of creation'), 157 | auto_now_add=True) 158 | 159 | 160 | class Meta: 161 | abstract = True 162 | 163 | 164 | objects = managers.BaseQuerySet.as_manager() 165 | pending = managers.PendingManager.from_queryset(managers.PendingQuerySet)() 166 | ended = managers.EndedManager.from_queryset(managers.BaseQuerySet)() 167 | 168 | 169 | @property 170 | def state(self): 171 | """ Get the state in workflow for the instance of the workflow managed 172 | model 173 | 174 | :return: the state of the managed instance 175 | :rtype: `workflows.models.State `_ 176 | """ 177 | return get_state(self) 178 | 179 | def change_state(self, transition, actor): 180 | """ Set new state for the instance of the workflow managed model 181 | 182 | :param transition: a transition object 183 | :type transition: `workflows.models.Transition `_ 184 | :param actor: a user object 185 | :type actor: `django.contrib.auth.User `_ 186 | 187 | This method send a signal to the application to notify a managed 188 | instance is changing state. The signal provides several arguments as 189 | the previous state, the executed transition and the actor. 190 | """ 191 | actual_state = self.state 192 | set_state(self, transition.destination) 193 | changed_state.send_robust(sender=self, transition=transition, 194 | actor=actor, previous_state=actual_state) 195 | 196 | @property 197 | def is_editable(self): 198 | """ Is this managed instance editable in fact of the state 199 | """ 200 | state = self.state 201 | return state is not None and \ 202 | state not in get_ending_states(state.workflow) 203 | 204 | def is_editable_by(self, user, permission='edit'): 205 | """ Is this managed instance editable by user in fact of state and his 206 | role permission 207 | 208 | :param user: a user object 209 | :type user: `django.contrib.auth.User `_ 210 | :param permission: the permisson to match 211 | :type permission: a string 212 | """ 213 | return self.is_editable and has_permission(self, user, permission) 214 | 215 | def allowed_transitions(self, user): 216 | """ Allowed transitions user can do on the managed instance 217 | 218 | :param user: a user object 219 | :type user: `django.contrib.auth.User `_ 220 | :return: allowed transitions 221 | :rtype: a list of `workflows.models.Transition `_ 222 | """ 223 | return get_allowed_transitions(self, user) 224 | 225 | def allowed_transition(self, transition_id, user): 226 | """ Allowed transition on managed instance based on a transition id 227 | check for the user trying to execute it 228 | 229 | :param transition_id: the transition_id the user wants to execute 230 | :type transition_id: an integer 231 | :param user: a user object 232 | :type user: `django.contrib.auth.User `_ 233 | :return: the transition if allowed 234 | :rtype: `workflows.models.Transition `_ 235 | """ 236 | for transition in self.allowed_transitions(user): 237 | if transition.id == transition_id: 238 | return transition 239 | return None 240 | 241 | def last_action(self): 242 | """ Last action on managed instance 243 | 244 | :return: the latest action on managed instance 245 | :rtype: :py:class:`arc.workflow_activity.Action` 246 | """ 247 | return self.actions.latest('process_date') 248 | 249 | def last_actor(self): 250 | """ Last actor on managed instance 251 | 252 | :return: a user object 253 | :rtype: `django.contrib.auth.User `_ 254 | """ 255 | try: 256 | return self.last_action().actor 257 | except Action.DoesNotExist: 258 | return None 259 | 260 | def last_transition(self): 261 | """ Last transition executed on managed instance 262 | 263 | :return: the transition 264 | :rtype: `workflows.models.Transition `_ 265 | """ 266 | try: 267 | return self.last_action().transition 268 | except Action.DoesNotExist: 269 | return None 270 | 271 | def last_state(self): 272 | """ Previous state of the managed instance 273 | 274 | :return: the previous state on the managed instance 275 | :rtype: `workflows.models.State `_ 276 | """ 277 | try: 278 | return self.last_action().previous_state 279 | except Action.DoesNotExist: 280 | return None 281 | 282 | def set_workflow(self, workflow): 283 | """ Initiate a workflow for instance. """ 284 | if self.state is None: 285 | if not workflow: 286 | ctype = ContentType.objects.get_for_model(self) 287 | workflow = get_workflow_for_model(ctype) 288 | set_workflow_for_object(self, workflow) 289 | 290 | def remove_workflow(self): 291 | """ Remove entirely a worflow for an instance. """ 292 | 293 | ctype = ContentType.objects.get_for_model(self) 294 | try: 295 | workflow = self.state.workflow 296 | wor = workflows.models.WorkflowObjectRelation.objects.get( 297 | content_type=ctype, content_id=self.pk 298 | ) 299 | sor = workflows.models.StateObjectRelation.objects.get( 300 | content_type=ctype, content_id=self.pk 301 | ) 302 | except workflows.models.WorkflowObjectRelation.DoesNotExist: 303 | pass 304 | except workflows.models.StateObjectRelation.DoesNotExist: 305 | pass 306 | 307 | with transaction.atomic(): 308 | wor.delete() 309 | sor.delete() 310 | 311 | 312 | @receiver(m2m_changed, sender=workflows.models.State.transitions.through) 313 | @receiver(post_save, sender=workflows.models.State) 314 | def update_ending_states(sender, **kwargs): 315 | """ When new states are created or new transitions are added to states, the 316 | ENDING_STATES static variable must be updated 317 | 318 | :param sender: the model that send the signal 319 | """ 320 | # signal send when a state is saved 321 | is_new_state = sender == workflows.models.State and kwargs['created'] 322 | # signal send when transitions are added to a state 323 | transition_added = sender == workflows.models.State.transitions.through \ 324 | and kwargs['action'] == 'post_add' 325 | 326 | if is_new_state or transition_added: 327 | from . import _ENDING_STATES 328 | workflow = kwargs['instance'].workflow 329 | # update the value for the right workflow in the static variable 330 | if workflow.name in _ENDING_STATES: 331 | del _ENDING_STATES[workflow.name] 332 | _ENDING_STATES[workflow.name] = get_ending_states(workflow) 333 | 334 | 335 | @receiver(changed_state) 336 | def create_action(sender, **kwargs): 337 | """ When a workflow managed instance is changing state, this function 338 | receive the signal and create a new action for the instance. Only model 339 | that inherits WorkflowManagedInstance will be matched to create actions 340 | 341 | :param sender: the instance that send the signal 342 | """ 343 | managed_instance = sender 344 | if managed_instance.__class__.__base__ == WorkflowManagedInstance: 345 | managed_instance.actions.create(transition=kwargs['transition'], 346 | actor=kwargs['actor'], previous_state=kwargs['previous_state'], 347 | workflow=kwargs['previous_state'].workflow) 348 | -------------------------------------------------------------------------------- /workflow_activity/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | workflow_activity.utils 5 | ======================= 6 | 7 | Utility functions for the workflow_activity application that can be used in the 8 | workflows application. 9 | """ 10 | 11 | from . import _ENDING_STATES 12 | 13 | 14 | def get_ending_states(workflow): 15 | """ Searches for the ending states of a workflow 16 | 17 | :param workflow: a workflow 18 | :type workflow: `workflows.models.Workflow `_ 19 | :return: a list of states 20 | :rtype: list of `workflows.models.State `_ 21 | """ 22 | ending_states = [] 23 | if workflow.name in _ENDING_STATES: 24 | ending_states = _ENDING_STATES[workflow.name] 25 | else: 26 | ending_states = workflow.states.filter(transitions__isnull=True) 27 | _ENDING_STATES[workflow.name] = ending_states 28 | return ending_states 29 | --------------------------------------------------------------------------------