├── .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 |
--------------------------------------------------------------------------------