Initial version
parent
88fb1fedbc
commit
5964845710
|
@ -0,0 +1,8 @@
|
||||||
|
.*.sw*
|
||||||
|
*.log
|
||||||
|
src/greenmine/settings/local.py
|
||||||
|
src/database.sqlite
|
||||||
|
src/logs
|
||||||
|
src/media
|
||||||
|
*.pyc
|
||||||
|
*.mo
|
|
@ -0,0 +1 @@
|
||||||
|
build
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Green-Mine.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Green-Mine.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/Green-Mine"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Green-Mine"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "!layout.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="deck">
|
||||||
|
|
||||||
|
{% if version == "0.7" or version == "0.8" %}
|
||||||
|
<p class="developmentversion">
|
||||||
|
This document is for Celery's development version, which can be
|
||||||
|
significantly different from previous releases. Get old docs here:
|
||||||
|
|
||||||
|
<a href="http://docs.celeryproject.org/en/latest/{{ pagename }}{{ file_suffix }}">2.5</a>.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
This document describes stdnet {{ version }}. For development docs,
|
||||||
|
<a href="http://lsbardel.github.com/python-stdnet/{{ pagename }}{{ file_suffix }}">go here</a>.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{ body }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{{ super() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
var _gaq = _gaq || [];
|
||||||
|
_gaq.push(['_setAccount', 'UA-3900561-6']);
|
||||||
|
_gaq.push(['_trackPageview']);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||||
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||||
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||||
|
})();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<h3>Green-Mine</h3>
|
||||||
|
<p>
|
||||||
|
Green-Mine is a project managment web application
|
||||||
|
build on top of django (1.4).
|
||||||
|
</p>
|
||||||
|
<h3>Useful Links</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/lsbardel/python-stdnet">Green-Mine @ github</a></li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<p class="logo">
|
||||||
|
<a href="{{ pathto(master_doc) }}">
|
||||||
|
<img class="logo" width="200" src="{{ pathto('_static/net.jpg', 1) }}" alt="Logo"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
|
@ -0,0 +1,394 @@
|
||||||
|
/*
|
||||||
|
* celery.css_t
|
||||||
|
* ~~~~~~~~~~~~
|
||||||
|
*
|
||||||
|
* :copyright: Copyright 2010 by Armin Ronacher.
|
||||||
|
* :license: BSD, see LICENSE for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
{% set page_width = 940 %}
|
||||||
|
{% set sidebar_width = 220 %}
|
||||||
|
{% set body_font_stack = 'Optima, Segoe, "Segoe UI", Candara, Calibri, Arial, sans-serif' %}
|
||||||
|
{% set headline_font_stack = 'Futura, "Trebuchet MS", Arial, sans-serif' %}
|
||||||
|
{% set code_font_stack = "'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace" %}
|
||||||
|
|
||||||
|
@import url("basic.css");
|
||||||
|
|
||||||
|
/* -- page layout ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: {{ body_font_stack }};
|
||||||
|
font-size: 17px;
|
||||||
|
background-color: white;
|
||||||
|
color: #000;
|
||||||
|
margin: 30px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.document {
|
||||||
|
width: {{ page_width }}px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related {
|
||||||
|
width: {{ page_width - 20 }}px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #F2FCEE;
|
||||||
|
margin: 15px auto 15px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.documentwrapper {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.bodywrapper {
|
||||||
|
margin: 0 0 0 {{ sidebar_width }}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar {
|
||||||
|
width: {{ sidebar_width }}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid #B1B4B6;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #3E4349;
|
||||||
|
padding: 0 30px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.celerylogo {
|
||||||
|
padding: 0 0 10px 10px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer {
|
||||||
|
width: {{ page_width - 15 }}px;
|
||||||
|
margin: 10px auto 30px auto;
|
||||||
|
padding-right: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer a {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar a {
|
||||||
|
color: #444;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px dashed #DCF0D5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar a:hover {
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebarwrapper {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebarwrapper p.logo {
|
||||||
|
padding: 0 0 20px 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h3,
|
||||||
|
div.sphinxsidebar h4 {
|
||||||
|
font-family: {{ headline_font_stack }};
|
||||||
|
color: #444;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h3 a {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p.logo a,
|
||||||
|
div.sphinxsidebar h3 a,
|
||||||
|
div.sphinxsidebar p.logo a:hover,
|
||||||
|
div.sphinxsidebar h3 a:hover {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p {
|
||||||
|
color: #555;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar input {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-family: {{ body_font_stack }};
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- body styles ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #348613;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #59B833;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body h1,
|
||||||
|
div.body h2,
|
||||||
|
div.body h3,
|
||||||
|
div.body h4,
|
||||||
|
div.body h5,
|
||||||
|
div.body h6 {
|
||||||
|
font-family: {{ headline_font_stack }};
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 30px 0px 10px 0px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body h1 { margin-top: 0; padding-top: 0; font-size: 200%; }
|
||||||
|
div.body h2 { font-size: 180%; }
|
||||||
|
div.body h3 { font-size: 150%; }
|
||||||
|
div.body h4 { font-size: 130%; }
|
||||||
|
div.body h5 { font-size: 100%; }
|
||||||
|
div.body h6 { font-size: 100%; }
|
||||||
|
|
||||||
|
div.body h1 a.toc-backref,
|
||||||
|
div.body h2 a.toc-backref,
|
||||||
|
div.body h3 a.toc-backref,
|
||||||
|
div.body h4 a.toc-backref,
|
||||||
|
div.body h5 a.toc-backref,
|
||||||
|
div.body h6 a.toc-backref {
|
||||||
|
color: inherit!important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.headerlink {
|
||||||
|
color: #ddd;
|
||||||
|
padding: 0 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.headerlink:hover {
|
||||||
|
color: #444;
|
||||||
|
background: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body p, div.body dd, div.body li {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition {
|
||||||
|
background: #fafafa;
|
||||||
|
margin: 20px -30px;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p.admonition-title {
|
||||||
|
font-family: {{ headline_font_stack }};
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.highlight{
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt:target, .highlight {
|
||||||
|
background: #FAF3E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.note {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.seealso {
|
||||||
|
background-color: #ffc;
|
||||||
|
border: 1px solid #ff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.warning {
|
||||||
|
background-color: #ffe4e4;
|
||||||
|
border: 1px solid #f66;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title:after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, tt {
|
||||||
|
font-family: {{ code_font_stack }};
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.screenshot {
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.descname, tt.descclassname {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.descname {
|
||||||
|
padding-right: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.screenshot {
|
||||||
|
-moz-box-shadow: 2px 2px 4px #eee;
|
||||||
|
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||||
|
box-shadow: 2px 2px 4px #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
border: 1px solid #888;
|
||||||
|
-moz-box-shadow: 2px 2px 4px #eee;
|
||||||
|
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||||
|
box-shadow: 2px 2px 4px #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.docutils td, table.docutils th {
|
||||||
|
border: 1px solid #888;
|
||||||
|
padding: 0.25em 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.field-list, table.footnote {
|
||||||
|
border: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.footnote {
|
||||||
|
margin: 15px 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background: #fdfdfd;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.footnote + table.footnote {
|
||||||
|
margin-top: -15px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.field-list th {
|
||||||
|
padding: 0 0.8em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.field-list td {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.footnote td.label {
|
||||||
|
width: 0px;
|
||||||
|
padding: 0.3em 0 0.3em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.footnote td {
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dd {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 0 30px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 10px 0 10px 30px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #F0FFEB;
|
||||||
|
padding: 7px 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border: 1px solid #C7ECB8;
|
||||||
|
border-radius: 2px;
|
||||||
|
-moz-border-radius: 2px;
|
||||||
|
-webkit-border-radius: 2px;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt {
|
||||||
|
background: #F0FFEB;
|
||||||
|
color: #222;
|
||||||
|
/* padding: 1px 2px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.xref, a tt {
|
||||||
|
background: #F0FFEB;
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.reference {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px dashed #DCF0D5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.reference:hover {
|
||||||
|
border-bottom: 1px solid #6D4100;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.footnote-reference {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.7em;
|
||||||
|
vertical-align: top;
|
||||||
|
border-bottom: 1px dashed #DCF0D5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.footnote-reference:hover {
|
||||||
|
border-bottom: 1px solid #6D4100;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover tt {
|
||||||
|
background: #EEE;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
[theme]
|
||||||
|
inherit = basic
|
||||||
|
stylesheet = celery.css
|
||||||
|
|
||||||
|
[options]
|
|
@ -0,0 +1,252 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Green-Mine documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Sun May 6 14:00:36 2012.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = []
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'greenmine'
|
||||||
|
copyright = u'2012, Andrei Antoukh'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '0.0.10'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '0.0.10'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
html_theme = 'default'
|
||||||
|
html_theme = 'celery'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
html_theme_path = ["_theme"]
|
||||||
|
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#html_sidebars = {}
|
||||||
|
html_sidebars = {
|
||||||
|
'index': ['sidebarlogo.html', 'sidebarintro.html',
|
||||||
|
'sourcelink.html', 'searchbox.html'],
|
||||||
|
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
||||||
|
'sourcelink.html', 'searchbox.html'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'greenminedoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'greenmine.tex', u'greenmine documentation',
|
||||||
|
u'Andrei Antoukh', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'green-mine', u'greenmine documentation',
|
||||||
|
[u'Andrei Antoukh'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ------------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
('index', 'greenmine', u'greenmine documentation',
|
||||||
|
u'Andrei Antoukh', 'greenmine', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
|
@ -0,0 +1,42 @@
|
||||||
|
==========
|
||||||
|
Green-Mine
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. rubric:: Project management web application build on top of Django.
|
||||||
|
|
||||||
|
Currently there is no stable version, but the project is already usable. All contributions and bug fixes is welcome.
|
||||||
|
|
||||||
|
|
||||||
|
First steps
|
||||||
|
===========
|
||||||
|
|
||||||
|
**From scratch:**
|
||||||
|
:ref:`Overview and Installation <intro-overview>`
|
||||||
|
|
||||||
|
**Tutorials:** TODO
|
||||||
|
|
||||||
|
**Miscellaneous:**
|
||||||
|
:ref:`Contributing <contributing>` |
|
||||||
|
:ref:`Tests <runtests>` |
|
||||||
|
:ref:`Changelog <changelog>` |
|
||||||
|
:ref:`License <license>`
|
||||||
|
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
overview.rst
|
||||||
|
settings.rst
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
.. _intro-overview:
|
||||||
|
|
||||||
|
========
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
* python 2.6 or 2.7
|
||||||
|
* django-superview >= 0.2
|
||||||
|
* psycopg2 >= 2.4 (if postgresql is used)
|
||||||
|
* pyzmq >= 2.2 (for async mailserver)
|
||||||
|
* sphinx >= 1.1.3 (for build this documentation)
|
||||||
|
* django >= 1.4 (builtin)
|
||||||
|
* markdown >= 2.1 (for markdown wiki)
|
||||||
|
* docutils >= 0.7 (for restructuredtext wiki)
|
||||||
|
|
||||||
|
Philosophy
|
||||||
|
==========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Installing
|
||||||
|
==========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Version Check
|
||||||
|
=============
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
.. _runtests:
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
=============
|
||||||
|
|
||||||
|
Requirements for running tests: same as standard requierements.
|
||||||
|
|
||||||
|
To run tests, open a shell on a package directory and type::
|
||||||
|
|
||||||
|
python manage.py test -v2 greenmine
|
||||||
|
|
||||||
|
To access coverage of tests you need to install the coverage_ package and run the tests using::
|
||||||
|
|
||||||
|
coverage run --omit=extern manage.py test -v2 greenmine
|
||||||
|
|
||||||
|
and to check out the coverage report::
|
||||||
|
|
||||||
|
coverage html
|
||||||
|
|
||||||
|
|
||||||
|
.. _contributing:
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Develpment of Green-Mine happens at github: https://github.com/niwibe/Green-Mine
|
||||||
|
|
||||||
|
We very much welcome your contribution of course. To do so, simply follow these guidelines:
|
||||||
|
|
||||||
|
1. Fork ``greenmine`` on github.
|
||||||
|
2. Create feature branch. Example: ``git checkout -b my_new_feature``
|
||||||
|
3. Push your changes. Example: ``git push -u origin my_new_feature``
|
||||||
|
4. Send me a pull-request.
|
||||||
|
|
||||||
|
.. _license:
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
This software is licensed under the New BSD_ License. See the LICENSE
|
||||||
|
file in the top distribution directory for the full license text.
|
||||||
|
|
||||||
|
.. _coverage: http://nedbatchelder.com/code/coverage/
|
||||||
|
.. _BSD: http://www.opensource.org/licenses/bsd-license.php
|
|
@ -0,0 +1,22 @@
|
||||||
|
Settings introduced by greenmine.
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Default settings
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The setting instance contains few default parameters used in throughout
|
||||||
|
the library. This parameters can be changed by the user by simply
|
||||||
|
overriding them.
|
||||||
|
|
||||||
|
.. attribute:: settings.HOST
|
||||||
|
|
||||||
|
Set a full host name, this is used for making urls for email
|
||||||
|
notifications. In the future, it will be automatic.
|
||||||
|
|
||||||
|
Default: ``"http://localhost:8000"`` (ready for developers)
|
||||||
|
|
||||||
|
.. attribute:: settings.DISABLE_REGISTRATION
|
||||||
|
|
||||||
|
Set this, disables user registration.
|
||||||
|
|
||||||
|
Default: ``False``
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db.models import signals
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from ..scrum.models import Project, UserStory, Task
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Centralized uuid attachment and ref generation
|
||||||
|
@receiver(signals.pre_save)
|
||||||
|
def attach_uuid(sender, instance, **kwargs):
|
||||||
|
fields = sender._meta.init_name_map()
|
||||||
|
#fields = sender._meta.get_all_field_names()
|
||||||
|
|
||||||
|
if "modified_date" in fields:
|
||||||
|
instance.modified_date = now()
|
||||||
|
|
||||||
|
# if sender class does not have uuid field.
|
||||||
|
if "uuid" in fields:
|
||||||
|
if not instance.uuid:
|
||||||
|
instance.uuid = unicode(uuid.uuid1())
|
||||||
|
|
||||||
|
|
||||||
|
# Centraliced reference assignation.
|
||||||
|
@receiver(signals.pre_save, sender=Task)
|
||||||
|
@receiver(signals.pre_save, sender=UserStory)
|
||||||
|
def attach_unique_reference(sender, instance, **kwargs):
|
||||||
|
project = Project.objects.select_for_update().filter(pk=instance.project_id).get()
|
||||||
|
if isinstance(instance, Task):
|
||||||
|
project.last_task_ref += 1
|
||||||
|
instance.ref = project.last_task_ref
|
||||||
|
else:
|
||||||
|
project.last_us_ref += 1
|
||||||
|
instance.ref = project.last_us_ref
|
||||||
|
|
||||||
|
project.save()
|
|
@ -0,0 +1,126 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.core import mail
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from ..models import *
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from greenqueue import send_task
|
||||||
|
|
||||||
|
|
||||||
|
class LowLevelEmailTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
mail.outbox = []
|
||||||
|
|
||||||
|
def test_send_one_mail(self):
|
||||||
|
send_task("send-mail", args = ["subject", "template", ["hola@niwi.be"]])
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
def test_send_bulk_mail(self):
|
||||||
|
send_task("send-bulk-mail", args = [[
|
||||||
|
('s1', 't1', ['hola@niwi.be']),
|
||||||
|
('s2', 't2', ['hola@niwi.be']),
|
||||||
|
]])
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMailTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user1 = User.objects.create(
|
||||||
|
username = 'test1',
|
||||||
|
email = 'test1@test.com',
|
||||||
|
is_active = True,
|
||||||
|
is_staff = True,
|
||||||
|
is_superuser = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user2 = User.objects.create(
|
||||||
|
username = 'test2',
|
||||||
|
email = 'test2@test.com',
|
||||||
|
is_active = True,
|
||||||
|
is_staff = False,
|
||||||
|
is_superuser = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user1.set_password("test")
|
||||||
|
self.user2.set_password("test")
|
||||||
|
|
||||||
|
self.user1.save()
|
||||||
|
self.user2.save()
|
||||||
|
|
||||||
|
mail.outbox = []
|
||||||
|
|
||||||
|
def test_remember_password(self):
|
||||||
|
url = reverse("remember-password")
|
||||||
|
|
||||||
|
post_params = {'email': 'test2@test.com'}
|
||||||
|
response = self.client.post(url, post_params, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
jdata = json.loads(response.content)
|
||||||
|
self.assertIn("valid", jdata)
|
||||||
|
self.assertTrue(jdata['valid'])
|
||||||
|
|
||||||
|
def test_remember_password_not_exists(self):
|
||||||
|
url = reverse("remember-password")
|
||||||
|
|
||||||
|
post_params = {'email': 'test2@testa.com'}
|
||||||
|
response = self.client.post(url, post_params, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
jdata = json.loads(response.content)
|
||||||
|
self.assertIn("valid", jdata)
|
||||||
|
self.assertFalse(jdata['valid'])
|
||||||
|
|
||||||
|
def test_send_recovery_password_by_staff(self):
|
||||||
|
url = reverse("users-recovery-password", args=[self.user2.pk])
|
||||||
|
|
||||||
|
ok = self.client.login(username="test1", password="test")
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
# pre test
|
||||||
|
self.assertTrue(self.user2.is_active)
|
||||||
|
self.assertEqual(self.user2.get_profile().token, None)
|
||||||
|
|
||||||
|
response = self.client.get(url, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# expected redirect
|
||||||
|
self.assertEqual(response.redirect_chain, [('http://testserver/users/2/edit/', 302)])
|
||||||
|
|
||||||
|
# test mail sending
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(mail.outbox[0].subject, "Greenmine: password recovery.")
|
||||||
|
|
||||||
|
# test user model modification
|
||||||
|
self.user2 = User.objects.get(pk=self.user2.pk)
|
||||||
|
self.assertTrue(self.user2.is_active)
|
||||||
|
self.assertFalse(self.user2.has_usable_password())
|
||||||
|
self.assertNotEqual(self.user2.get_profile().token, None)
|
||||||
|
|
||||||
|
url = reverse('password-recovery', args=[self.user2.get_profile().token])
|
||||||
|
|
||||||
|
post_params = {
|
||||||
|
'password': '123123',
|
||||||
|
'password2': '123123',
|
||||||
|
}
|
||||||
|
response = self.client.post(url, post_params, follow=True)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# expected redirect
|
||||||
|
self.assertEqual(response.redirect_chain, [('http://testserver/login/', 302)])
|
||||||
|
|
||||||
|
ok = self.client.login(username="test2", password="123123")
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from greenmine.wiki.fields import WikiField
|
||||||
|
from greenmine.core.utils.slug import slugify_uniquely as slugify
|
||||||
|
from greenmine.taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
|
||||||
|
class Document(models.Model):
|
||||||
|
title = models.CharField(max_length=150)
|
||||||
|
slug = models.SlugField(unique=True, max_length=200, blank=True)
|
||||||
|
description = WikiField(blank=True)
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
project = models.ForeignKey('scrum.Project', related_name='documents')
|
||||||
|
owner = models.ForeignKey('auth.User', related_name='documents')
|
||||||
|
attached_file = models.FileField(upload_to="documents",
|
||||||
|
max_length=1000, null=True, blank=True)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.title, self.__class__)
|
||||||
|
super(Document, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('documents-delete', (),
|
||||||
|
{'pslug': self.project.slug, 'docid': self.pk})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return self.attached_file.url
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
from haystack import indexes
|
||||||
|
from .models import Document
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentIndex(indexes.RealTimeSearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/document_text.txt')
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return Document
|
||||||
|
|
||||||
|
def index_queryset(self):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{ object.title }}
|
||||||
|
{{ object.slug }}
|
||||||
|
{{ object.description }}
|
||||||
|
{{ object.created_date }}
|
||||||
|
{{ object.modified_date }}
|
||||||
|
{{ object.project }}
|
||||||
|
{{ object.owner }}
|
||||||
|
{{ object.attached_file }}
|
|
@ -0,0 +1,2 @@
|
||||||
|
i# -*- coding: utf-8 -*-
|
||||||
|
from .documents import *
|
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.core import mail
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from ..models import *
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "profile.role",
|
||||||
|
"fields": {
|
||||||
|
"name": "Developer",
|
||||||
|
"slug": "developer",
|
||||||
|
"project_view": true,
|
||||||
|
"project_edit": false,
|
||||||
|
"project_delete": false,
|
||||||
|
"userstory_view": true,
|
||||||
|
"userstory_create": true,
|
||||||
|
"userstory_edit": true,
|
||||||
|
"userstory_delete": true,
|
||||||
|
"milestone_view": true,
|
||||||
|
"milestone_create": true,
|
||||||
|
"milestone_edit": true,
|
||||||
|
"milestone_delete": true,
|
||||||
|
"task_view": true,
|
||||||
|
"task_create": true,
|
||||||
|
"task_edit": true,
|
||||||
|
"task_delete": true,
|
||||||
|
"wiki_view": true,
|
||||||
|
"wiki_create": true,
|
||||||
|
"wiki_edit": true,
|
||||||
|
"wiki_delete": true,
|
||||||
|
"question_view": true,
|
||||||
|
"question_create": true,
|
||||||
|
"question_edit": true,
|
||||||
|
"question_delete": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 2,
|
||||||
|
"model": "profile.role",
|
||||||
|
"fields": {
|
||||||
|
"name": "Product Owner",
|
||||||
|
"slug": "product-owner",
|
||||||
|
"project_view": true,
|
||||||
|
"project_edit": false,
|
||||||
|
"project_delete": false,
|
||||||
|
"userstory_view": true,
|
||||||
|
"userstory_create": true,
|
||||||
|
"userstory_edit": true,
|
||||||
|
"userstory_delete": true,
|
||||||
|
"milestone_view": true,
|
||||||
|
"milestone_create": false,
|
||||||
|
"milestone_edit": false,
|
||||||
|
"milestone_delete": false,
|
||||||
|
"task_view": true,
|
||||||
|
"task_create": false,
|
||||||
|
"task_edit": false,
|
||||||
|
"task_delete": false,
|
||||||
|
"wiki_view": true,
|
||||||
|
"wiki_create": true,
|
||||||
|
"wiki_edit": true,
|
||||||
|
"wiki_delete": true,
|
||||||
|
"question_view": true,
|
||||||
|
"question_create": true,
|
||||||
|
"question_edit": true,
|
||||||
|
"question_delete": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 3,
|
||||||
|
"model": "profile.role",
|
||||||
|
"fields": {
|
||||||
|
"name": "Observer",
|
||||||
|
"slug": "observer",
|
||||||
|
"project_view": true,
|
||||||
|
"project_edit": false,
|
||||||
|
"project_delete": false,
|
||||||
|
"userstory_view": true,
|
||||||
|
"userstory_create": false,
|
||||||
|
"userstory_edit": false,
|
||||||
|
"userstory_delete": false,
|
||||||
|
"milestone_view": true,
|
||||||
|
"milestone_create": false,
|
||||||
|
"milestone_edit": false,
|
||||||
|
"milestone_delete": false,
|
||||||
|
"task_view": true,
|
||||||
|
"task_create": false,
|
||||||
|
"task_edit": false,
|
||||||
|
"task_delete": false,
|
||||||
|
"wiki_view": true,
|
||||||
|
"wiki_create": false,
|
||||||
|
"wiki_edit": false,
|
||||||
|
"wiki_delete": false,
|
||||||
|
"question_view": true,
|
||||||
|
"question_create": false,
|
||||||
|
"question_edit": false,
|
||||||
|
"question_delete": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ugettext
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from greenmine.core.fields import DictField, ListField
|
||||||
|
from greenmine.wiki.fields import WikiField
|
||||||
|
from greenmine.core.utils import iter_points
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
user = models.OneToOneField("auth.User", related_name='profile')
|
||||||
|
description = WikiField(blank=True)
|
||||||
|
photo = models.FileField(upload_to="files/msg",
|
||||||
|
max_length=500, null=True, blank=True)
|
||||||
|
|
||||||
|
default_language = models.CharField(max_length=20,
|
||||||
|
null=True, blank=True, default=None)
|
||||||
|
default_timezone = models.CharField(max_length=20,
|
||||||
|
null=True, blank=True, default=None)
|
||||||
|
token = models.CharField(max_length=200, unique=True,
|
||||||
|
null=True, blank=True, default=None)
|
||||||
|
colorize_tags = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Role(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
slug = models.SlugField(max_length=250, unique=True, blank=True)
|
||||||
|
|
||||||
|
project_view = models.BooleanField(default=True)
|
||||||
|
project_edit = models.BooleanField(default=False)
|
||||||
|
project_delete = models.BooleanField(default=False)
|
||||||
|
userstory_view = models.BooleanField(default=True)
|
||||||
|
userstory_create = models.BooleanField(default=False)
|
||||||
|
userstory_edit = models.BooleanField(default=False)
|
||||||
|
userstory_delete = models.BooleanField(default=False)
|
||||||
|
milestone_view = models.BooleanField(default=True)
|
||||||
|
milestone_create = models.BooleanField(default=False)
|
||||||
|
milestone_edit = models.BooleanField(default=False)
|
||||||
|
milestone_delete = models.BooleanField(default=False)
|
||||||
|
task_view = models.BooleanField(default=True)
|
||||||
|
task_create = models.BooleanField(default=False)
|
||||||
|
task_edit = models.BooleanField(default=False)
|
||||||
|
task_delete = models.BooleanField(default=False)
|
||||||
|
wiki_view = models.BooleanField(default=True)
|
||||||
|
wiki_create = models.BooleanField(default=False)
|
||||||
|
wiki_edit = models.BooleanField(default=False)
|
||||||
|
wiki_delete = models.BooleanField(default=False)
|
||||||
|
question_view = models.BooleanField(default=True)
|
||||||
|
question_create = models.BooleanField(default=True)
|
||||||
|
question_edit = models.BooleanField(default=True)
|
||||||
|
question_delete = models.BooleanField(default=False)
|
||||||
|
document_view = models.BooleanField(default=True)
|
||||||
|
document_create = models.BooleanField(default=True)
|
||||||
|
document_edit = models.BooleanField(default=True)
|
||||||
|
document_delete = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
from . import sigdispatch
|
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from .models import Profile
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def user_post_save(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Create void user profile if instance is a new user.
|
||||||
|
"""
|
||||||
|
if created and not Profile.objects.filter(user=instance).exists():
|
||||||
|
Profile.objects.create(user=instance)
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
|
@ -0,0 +1,65 @@
|
||||||
|
from django.db import models
|
||||||
|
from greenmine.core.utils.slug import slugify_uniquely
|
||||||
|
from greenmine.wiki.fields import WikiField
|
||||||
|
from greenmine.taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
|
||||||
|
class Question(models.Model):
|
||||||
|
subject = models.CharField(max_length=150)
|
||||||
|
slug = models.SlugField(unique=True, max_length=250, blank=True)
|
||||||
|
content = WikiField(blank=True)
|
||||||
|
closed = models.BooleanField(default=False)
|
||||||
|
attached_file = models.FileField(upload_to="messages",
|
||||||
|
max_length=500, null=True, blank=True)
|
||||||
|
|
||||||
|
project = models.ForeignKey('scrum.Project', related_name='questions')
|
||||||
|
milestone = models.ForeignKey('scrum.Milestone', related_name='questions',
|
||||||
|
null=True, default=None, blank=True)
|
||||||
|
|
||||||
|
assigned_to = models.ForeignKey("auth.User")
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
owner = models.ForeignKey('auth.User', related_name='questions')
|
||||||
|
|
||||||
|
|
||||||
|
watchers = models.ManyToManyField('auth.User',
|
||||||
|
related_name='question_watch', null=True, blank=True)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return self.get_view_url()
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_view_url(self):
|
||||||
|
return ('questions-view', (),
|
||||||
|
{'pslug': self.project.slug, 'qslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
return ('questions-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'qslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('questions-delete', (),
|
||||||
|
{'pslug': self.project.slug, 'qslug': self.slug})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify_uniquely(self.subject, self.__class__)
|
||||||
|
super(Question, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionResponse(models.Model):
|
||||||
|
content = WikiField()
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
attached_file = models.FileField(upload_to="messages",
|
||||||
|
max_length=500, null=True, blank=True)
|
||||||
|
|
||||||
|
question = models.ForeignKey('Question', related_name='responses')
|
||||||
|
owner = models.ForeignKey('auth.User', related_name='questions_responses')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
from haystack import indexes
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionIndex(indexes.RealTimeSearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/question_text.txt')
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return Question
|
||||||
|
|
||||||
|
def index_queryset(self):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{ object.subject }}
|
||||||
|
{{ object.slug }}
|
||||||
|
{{ object.content }}
|
||||||
|
{{ object.attached_file }}
|
||||||
|
{{ object.project }}
|
||||||
|
{{ object.milestone }}
|
||||||
|
{{ object.assigned_to }}
|
||||||
|
{{ object.created_date }}
|
||||||
|
{{ object.modified_date }}
|
||||||
|
{{ object.owner }}
|
||||||
|
{% for watcher in object.watchers.all %}
|
||||||
|
{{ watcher }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for response in object.responses.all %}
|
||||||
|
{{ response.content }}
|
||||||
|
{{ response.created_date }}
|
||||||
|
{{ response.modified_date }}
|
||||||
|
{{ response.attached_file }}
|
||||||
|
{{ response.owner }}
|
||||||
|
{% endfor %}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .utils import SCRUM_STATES
|
||||||
|
|
||||||
|
ORG_ROLE_CHOICES = (
|
||||||
|
('owner', _(u'Owner')),
|
||||||
|
('developer', _(u'Developer')),
|
||||||
|
)
|
||||||
|
|
||||||
|
MARKUP_TYPE = (
|
||||||
|
('md', _(u'Markdown')),
|
||||||
|
('rst', _('Restructured Text')),
|
||||||
|
)
|
||||||
|
|
||||||
|
US_STATUS_CHOICES = SCRUM_STATES.get_us_choices()
|
||||||
|
|
||||||
|
TASK_PRIORITY_CHOICES = (
|
||||||
|
(1, _(u'Low')),
|
||||||
|
(3, _(u'Normal')),
|
||||||
|
(5, _(u'High')),
|
||||||
|
)
|
||||||
|
|
||||||
|
TASK_SEVERITY_CHOICES = (
|
||||||
|
(1, _(u'Wishlist')),
|
||||||
|
(2, _(u'Minor')),
|
||||||
|
(3, _(u'Normal')),
|
||||||
|
(4, _(u'Important')),
|
||||||
|
(5, _(u'Critical')),
|
||||||
|
)
|
||||||
|
|
||||||
|
TASK_TYPE_CHOICES = (
|
||||||
|
('bug', _(u'Bug')),
|
||||||
|
('task', _(u'Task')),
|
||||||
|
)
|
||||||
|
|
||||||
|
TASK_STATUS_CHOICES = SCRUM_STATES.get_task_choices()
|
||||||
|
|
||||||
|
POINTS_CHOICES = (
|
||||||
|
(-1, u'?'),
|
||||||
|
(0, u'0'),
|
||||||
|
(-2, u'1/2'),
|
||||||
|
(1, u'1'),
|
||||||
|
(2, u'2'),
|
||||||
|
(3, u'3'),
|
||||||
|
(5, u'5'),
|
||||||
|
(8, u'8'),
|
||||||
|
(10, u'10'),
|
||||||
|
(15, u'15'),
|
||||||
|
(20, u'20'),
|
||||||
|
(40, u'40'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TASK_COMMENT = 1
|
||||||
|
TASK_STATUS_CHANGE = 2
|
||||||
|
TASK_PRIORITY_CHANGE = 3
|
||||||
|
TASK_ASSIGNATION_CHANGE = 4
|
||||||
|
|
||||||
|
TASK_CHANGE_CHOICES = (
|
||||||
|
(TASK_COMMENT, _(u"Task comment")),
|
||||||
|
(TASK_STATUS_CHANGE, _(u"Task status change")),
|
||||||
|
(TASK_PRIORITY_CHANGE, _(u"Task prioriy change")),
|
||||||
|
(TASK_ASSIGNATION_CHANGE, _(u"Task assignation change")),
|
||||||
|
)
|
|
@ -0,0 +1,38 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "auth.user",
|
||||||
|
"fields": {
|
||||||
|
"username": "andrei",
|
||||||
|
"first_name": "Andrei",
|
||||||
|
"last_name": "Antoukh",
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": true,
|
||||||
|
"is_staff": true,
|
||||||
|
"last_login": "2011-08-18T19:53:26Z",
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": [],
|
||||||
|
"password": "sha1$DytEojXM0V24$3af4d8cda73cd08525871de8c9b622a39a6faed0",
|
||||||
|
"email": "niwi@niwi.be",
|
||||||
|
"date_joined": "2011-08-18T19:53:26Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 2,
|
||||||
|
"model": "auth.user",
|
||||||
|
"fields": {
|
||||||
|
"username": "juanfran",
|
||||||
|
"first_name": "Juan Fran",
|
||||||
|
"last_name": "Alcantara",
|
||||||
|
"is_active": true,
|
||||||
|
"is_superuser": true,
|
||||||
|
"is_staff": true,
|
||||||
|
"last_login": "2011-08-18T19:53:26Z",
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": [],
|
||||||
|
"password": "sha1$DytEojXM0V24$3af4d8cda73cd08525871de8c9b622a39a6faed0",
|
||||||
|
"email": "juanfran@niwi.be",
|
||||||
|
"date_joined": "2011-08-18T19:53:26Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,143 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
from django.core import management
|
||||||
|
from django.contrib.webdesign import lorem_ipsum
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
import random, sys, datetime
|
||||||
|
|
||||||
|
from greenmine.base.models import *
|
||||||
|
from greenmine.scrum.models import *
|
||||||
|
|
||||||
|
subjects = [
|
||||||
|
"Fixing templates for Django 1.2.",
|
||||||
|
"get_actions() does not check for 'delete_selected' in actions",
|
||||||
|
"Experimental: modular file types",
|
||||||
|
"Add setting to allow regular users to create folders at the root level.",
|
||||||
|
"add tests for bulk operations",
|
||||||
|
"create testsuite with matrix builds",
|
||||||
|
"Lighttpd support",
|
||||||
|
"Lighttpd x-sendfile support",
|
||||||
|
"Added file copying and processing of images (resizing)",
|
||||||
|
"Exception is thrown if trying to add a folder with existing name",
|
||||||
|
"Feature/improved image admin",
|
||||||
|
"Support for bulk actions",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
@transaction.commit_on_success
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
from django.core import management
|
||||||
|
management.call_command('loaddata', 'development_users')
|
||||||
|
users_counter = 0
|
||||||
|
|
||||||
|
def create_user(counter):
|
||||||
|
user = User.objects.create(
|
||||||
|
username = 'foouser%d' % (counter),
|
||||||
|
first_name = 'foouser%d' % (counter),
|
||||||
|
email = 'foouser%d@foodomain.com' % (counter),
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
# projects
|
||||||
|
for x in xrange(3):
|
||||||
|
# create project
|
||||||
|
project = Project.objects.create(
|
||||||
|
name = 'Project Example %s' % (x),
|
||||||
|
description = 'Project example %s description' % (x),
|
||||||
|
owner = random.choice(list(User.objects.all()[:1])),
|
||||||
|
public = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
project.add_user(project.owner, "developer")
|
||||||
|
|
||||||
|
extras = project.get_extras()
|
||||||
|
extras.show_burndown = True
|
||||||
|
extras.show_sprint_burndown = True
|
||||||
|
extras.sprints = 4
|
||||||
|
extras.save()
|
||||||
|
|
||||||
|
# add random participants to project
|
||||||
|
participants = []
|
||||||
|
for t in xrange(random.randint(1, 2)):
|
||||||
|
participant = create_user(users_counter)
|
||||||
|
participants.append(participant)
|
||||||
|
|
||||||
|
project.add_user(participant, "developer")
|
||||||
|
users_counter += 1
|
||||||
|
|
||||||
|
now_date = now() - datetime.timedelta(30)
|
||||||
|
|
||||||
|
# create random milestones
|
||||||
|
for y in xrange(2):
|
||||||
|
milestone = Milestone.objects.create(
|
||||||
|
project = project,
|
||||||
|
name = 'Sprint %s' % (y),
|
||||||
|
owner = project.owner,
|
||||||
|
created_date = now_date,
|
||||||
|
modified_date = now_date,
|
||||||
|
estimated_start = now_date,
|
||||||
|
estimated_finish = now_date + datetime.timedelta(15)
|
||||||
|
)
|
||||||
|
|
||||||
|
now_date = now_date + datetime.timedelta(15)
|
||||||
|
|
||||||
|
# create uss asociated to milestones
|
||||||
|
for z in xrange(5):
|
||||||
|
us = UserStory.objects.create(
|
||||||
|
subject = lorem_ipsum.words(random.randint(4,9), common=False),
|
||||||
|
priority = 6,
|
||||||
|
points = 3,
|
||||||
|
project = project,
|
||||||
|
owner = random.choice(participants),
|
||||||
|
description = lorem_ipsum.words(30, common=False),
|
||||||
|
milestone = milestone,
|
||||||
|
status = 'completed',
|
||||||
|
)
|
||||||
|
for tag in lorem_ipsum.words(random.randint(1,5), common=True).split(" "):
|
||||||
|
us.tags.add(tag)
|
||||||
|
|
||||||
|
for w in xrange(3):
|
||||||
|
task = Task.objects.create(
|
||||||
|
subject = "Task %s" % (w),
|
||||||
|
description = lorem_ipsum.words(30, common=False),
|
||||||
|
project = project,
|
||||||
|
owner = random.choice(participants),
|
||||||
|
milestone = milestone,
|
||||||
|
user_story = us,
|
||||||
|
status = 'completed',
|
||||||
|
)
|
||||||
|
|
||||||
|
# created unassociated uss.
|
||||||
|
for y in xrange(10):
|
||||||
|
us = UserStory.objects.create(
|
||||||
|
subject = lorem_ipsum.words(random.randint(4,9), common=False),
|
||||||
|
priority = 3,
|
||||||
|
points = 3,
|
||||||
|
status = 'open',
|
||||||
|
owner = random.choice(participants),
|
||||||
|
description = lorem_ipsum.words(30, common=False),
|
||||||
|
milestone = None,
|
||||||
|
project = project,
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in lorem_ipsum.words(random.randint(1,5), common=True).split(" "):
|
||||||
|
us.tags.add(tag)
|
||||||
|
|
||||||
|
# create bugs.
|
||||||
|
for y in xrange(20):
|
||||||
|
bug = Task.objects.create(
|
||||||
|
project = project,
|
||||||
|
type = "bug",
|
||||||
|
severity = random.randint(1,5),
|
||||||
|
subject = lorem_ipsum.words(random.randint(1,5), common=False),
|
||||||
|
description = lorem_ipsum.words(random.randint(1,15), common=False),
|
||||||
|
owner = project.owner,
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in lorem_ipsum.words(random.randint(1,5), common=True).split(" "):
|
||||||
|
bug.tags.add(tag)
|
||||||
|
|
|
@ -0,0 +1,809 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ugettext
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.contenttypes import generic
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.models import UserManager
|
||||||
|
|
||||||
|
from greenmine.core.utils.slug import slugify_uniquely, ref_uniquely
|
||||||
|
from greenmine.core.fields import DictField, ListField
|
||||||
|
from greenmine.wiki.fields import WikiField
|
||||||
|
from greenmine.core.utils import iter_points
|
||||||
|
from greenmine.taggit.managers import TaggableManager
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
from .choices import *
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .utils import SCRUM_STATES
|
||||||
|
|
||||||
|
class ProjectManager(models.Manager):
|
||||||
|
def get_by_natural_key(self, slug):
|
||||||
|
return self.get(slug=slug)
|
||||||
|
|
||||||
|
def can_view(self, user):
|
||||||
|
queryset = ProjectUserRole.objects.filter(user=user)\
|
||||||
|
.values_list('project', flat=True)
|
||||||
|
return Project.objects.filter(pk__in=queryset)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectExtras(models.Model):
|
||||||
|
task_parser_re = models.CharField(max_length=1000, blank=True, null=True, default=None)
|
||||||
|
sprints = models.IntegerField(default=1, blank=True, null=True)
|
||||||
|
show_burndown = models.BooleanField(default=False, blank=True)
|
||||||
|
show_burnup = models.BooleanField(default=False, blank=True)
|
||||||
|
show_sprint_burndown = models.BooleanField(default=False, blank=True)
|
||||||
|
total_story_points = models.FloatField(default=None, null=True)
|
||||||
|
|
||||||
|
def get_task_parse_re(self):
|
||||||
|
re_str = settings.DEFAULT_TASK_PARSER_RE
|
||||||
|
if self.task_parser_re:
|
||||||
|
re_str = self.task_parser_re
|
||||||
|
return re.compile(re_str, flags=re.U+re.M)
|
||||||
|
|
||||||
|
def parse_ustext(self, text):
|
||||||
|
rx = self.get_task_parse_re()
|
||||||
|
texts = rx.findall(text)
|
||||||
|
for text in texts:
|
||||||
|
yield text
|
||||||
|
|
||||||
|
|
||||||
|
class Project(models.Model):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
||||||
|
name = models.CharField(max_length=250, unique=True)
|
||||||
|
slug = models.SlugField(max_length=250, unique=True, blank=True)
|
||||||
|
description = WikiField(blank=False)
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
owner = models.ForeignKey("auth.User", related_name="projects")
|
||||||
|
participants = models.ManyToManyField('auth.User',
|
||||||
|
related_name="projects_participant", through="ProjectUserRole",
|
||||||
|
null=True, blank=True)
|
||||||
|
|
||||||
|
public = models.BooleanField(default=True)
|
||||||
|
markup = models.CharField(max_length=10, choices=MARKUP_TYPE, default='md')
|
||||||
|
extras = models.OneToOneField("ProjectExtras", related_name="project", null=True, default=None)
|
||||||
|
|
||||||
|
last_us_ref = models.BigIntegerField(null=True, default=0)
|
||||||
|
last_task_ref = models.BigIntegerField(null=True, default=0)
|
||||||
|
|
||||||
|
objects = ProjectManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_extras(self):
|
||||||
|
if self.extras is None:
|
||||||
|
self.extras = ProjectExtras.objects.create()
|
||||||
|
self.__class__.objects.filter(pk=self.pk).update(extras=self.extras)
|
||||||
|
|
||||||
|
return self.extras
|
||||||
|
|
||||||
|
def natural_key(self):
|
||||||
|
return (self.slug,)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unasociated_user_stories(self):
|
||||||
|
return self.user_stories.filter(milestone__isnull=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_participants(self):
|
||||||
|
qs = ProjectUserRole.objects.filter(project=self)
|
||||||
|
return User.objects.filter(id__in=qs.values_list('user__pk', flat=True))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_milestone(self):
|
||||||
|
return self.milestones.order_by('-created_date')[0]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<Project %s>" % (self.slug)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify_uniquely(self.name, self.__class__)
|
||||||
|
else:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
|
||||||
|
super(Project, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def add_user(self, user, role):
|
||||||
|
from greenmine.core import permissions
|
||||||
|
return ProjectUserRole.objects.create(
|
||||||
|
project = self,
|
||||||
|
user = user,
|
||||||
|
role = permissions.get_role(role),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
""" Permalinks """
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return ('project-backlog', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_dashboard_url(self):
|
||||||
|
return ('dashboard', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_url(self):
|
||||||
|
return ('project-backlog', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_stats_url(self):
|
||||||
|
return ('project-backlog-stats', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_left_block_url(self):
|
||||||
|
return ('project-backlog-left-block', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_right_block_url(self):
|
||||||
|
return ('project-backlog-right-block', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_burndown_url(self):
|
||||||
|
return ('project-backlog-burndown', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_backlog_burnup_url(self):
|
||||||
|
return ('project-backlog-burnup', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_milestone_create_url(self):
|
||||||
|
return ('milestone-create', (),
|
||||||
|
{'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_userstory_create_url(self):
|
||||||
|
return ('user-story-create', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
return ('project-edit', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('project-delete', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_export_url(self):
|
||||||
|
return ('project-export-settings', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_export_now_url(self):
|
||||||
|
return ('project-export-settings-now', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_export_rehash_url(self):
|
||||||
|
return ('project-export-settings-rehash', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_issues_url(self):
|
||||||
|
return ('issues-list', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_settings_url(self):
|
||||||
|
return ('project-personal-settings', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_general_settings_url(self):
|
||||||
|
return ('project-general-settings', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_questions_url(self):
|
||||||
|
return ('questions', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_questions_create_url(self):
|
||||||
|
return ('questions-create', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_documents_url(self):
|
||||||
|
return ('documents', (), {'pslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_wiki_url(self):
|
||||||
|
return ('wiki-page', (), {'pslug': self.slug, 'wslug': 'index'})
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectUserRole(models.Model):
|
||||||
|
project = models.ForeignKey("Project", related_name="user_roles")
|
||||||
|
user = models.ForeignKey("auth.User", related_name="user_roles")
|
||||||
|
role = models.ForeignKey("profile.Role", related_name="user_roles")
|
||||||
|
|
||||||
|
mail_milestone_created = models.BooleanField(default=True)
|
||||||
|
mail_milestone_modified = models.BooleanField(default=False)
|
||||||
|
mail_milestone_deleted = models.BooleanField(default=False)
|
||||||
|
mail_userstory_created = models.BooleanField(default=True)
|
||||||
|
mail_userstory_modified = models.BooleanField(default=False)
|
||||||
|
mail_userstory_deleted = models.BooleanField(default=False)
|
||||||
|
mail_task_created = models.BooleanField(default=True)
|
||||||
|
mail_task_assigned = models.BooleanField(default=False)
|
||||||
|
mail_task_deleted = models.BooleanField(default=False)
|
||||||
|
mail_question_created = models.BooleanField(default=False)
|
||||||
|
mail_question_assigned = models.BooleanField(default=True)
|
||||||
|
mail_question_deleted = models.BooleanField(default=False)
|
||||||
|
mail_document_created = models.BooleanField(default=True)
|
||||||
|
mail_document_deleted = models.BooleanField(default=False)
|
||||||
|
mail_wiki_created = models.BooleanField(default=False)
|
||||||
|
mail_wiki_modified = models.BooleanField(default=False)
|
||||||
|
mail_wiki_deleted = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<Project-User-Relation-%s>" % (self.id)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('project', 'user')
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneManager(models.Manager):
|
||||||
|
def get_by_natural_key(self, name, project):
|
||||||
|
return self.get(name=name, project__slug=project)
|
||||||
|
|
||||||
|
|
||||||
|
class Milestone(models.Model):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
||||||
|
name = models.CharField(max_length=200, db_index=True)
|
||||||
|
owner = models.ForeignKey('auth.User', related_name="milestones")
|
||||||
|
project = models.ForeignKey('Project', related_name="milestones")
|
||||||
|
|
||||||
|
estimated_start = models.DateField(null=True, default=None)
|
||||||
|
estimated_finish = models.DateField(null=True, default=None)
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
closed = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
disponibility = models.FloatField(null=True, default=0.0)
|
||||||
|
objects = MilestoneManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-created_date']
|
||||||
|
unique_together = ('name', 'project')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_points(self):
|
||||||
|
"""
|
||||||
|
Get total story points for this milestone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
total = sum(iter_points(self.user_stories.all()))
|
||||||
|
return "{0:.1f}".format(total)
|
||||||
|
|
||||||
|
def get_points_done_at_date(self, date):
|
||||||
|
"""
|
||||||
|
Get completed story points for this milestone before the date.
|
||||||
|
"""
|
||||||
|
total = 0.0
|
||||||
|
|
||||||
|
for item in self.user_stories.filter(status__in=SCRUM_STATES.get_finished_us_states()):
|
||||||
|
if item.tasks.filter(finished_date__lt=date).count() > 0:
|
||||||
|
if item.points == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.points == -2:
|
||||||
|
total += 0.5
|
||||||
|
continue
|
||||||
|
|
||||||
|
total += item.points
|
||||||
|
|
||||||
|
return "{0:.1f}".format(total)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed_points(self):
|
||||||
|
"""
|
||||||
|
Get a total of completed points.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = self.user_stories.filter(status__in=SCRUM_STATES.get_finished_us_states())
|
||||||
|
total = sum(iter_points(queryset))
|
||||||
|
return "{0:.1f}".format(total)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage_completed(self):
|
||||||
|
return "{0:.1f}".format(
|
||||||
|
(float(self.completed_points) * 100) / float(self.total_points)
|
||||||
|
)
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return ('dashboard', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
return ('milestone-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('milestone-delete', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_dashboard_url(self):
|
||||||
|
return ('dashboard', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_stats_url(self):
|
||||||
|
return ('dashboard-api-stats', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_user_story_create_url(self):
|
||||||
|
return ('user-story-create', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_ml_detail_url(self):
|
||||||
|
return ('milestone-dashboard', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_create_task_url(self):
|
||||||
|
# TODO: deprecated
|
||||||
|
return ('api:task-create', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_stats_api_url(self):
|
||||||
|
return ('api:stats-milestone', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_tasks_url(self):
|
||||||
|
return ('tasks-view', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_tasks_url_filter_by_task(self):
|
||||||
|
return ('tasks-view', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id, 'filter_by':'task'})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_tasks_url_filter_by_bug(self):
|
||||||
|
return ('tasks-view', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id, 'filter_by':'bug'})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_task_create_url(self):
|
||||||
|
return ('task-create', (),
|
||||||
|
{'pslug': self.project.slug, 'mid': self.id})
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
unique_together = ('name', 'project')
|
||||||
|
|
||||||
|
def natural_key(self):
|
||||||
|
return (self.name,) + self.project.natural_key()
|
||||||
|
|
||||||
|
natural_key.dependencies = ['greenmine.Project']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<Milestone %s>" % (self.id)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.id:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
|
||||||
|
super(Milestone, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStory(models.Model):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
||||||
|
ref = models.CharField(max_length=200, db_index=True, null=True, default=None)
|
||||||
|
milestone = models.ForeignKey("Milestone", blank=True,
|
||||||
|
related_name="user_stories", null=True, default=None)
|
||||||
|
project = models.ForeignKey("Project", related_name="user_stories")
|
||||||
|
owner = models.ForeignKey("auth.User", null=True,
|
||||||
|
default=None, related_name="user_stories")
|
||||||
|
priority = models.IntegerField(default=1)
|
||||||
|
points = models.IntegerField(choices=POINTS_CHOICES, default=-1)
|
||||||
|
status = models.CharField(max_length=50,
|
||||||
|
choices=SCRUM_STATES.get_us_choices(), db_index=True, default="open")
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
tested = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
subject = models.CharField(max_length=500)
|
||||||
|
description = WikiField()
|
||||||
|
finish_date = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
watchers = models.ManyToManyField('auth.User',
|
||||||
|
related_name='us_watch', null=True)
|
||||||
|
|
||||||
|
client_requirement = models.BooleanField(default=False)
|
||||||
|
team_requirement = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('ref', 'project')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.pk,
|
||||||
|
"ref": self.ref,
|
||||||
|
"subject": self.subject,
|
||||||
|
"viewUrl": self.get_view_url(),
|
||||||
|
"pointsDisplay": self.get_points_display(),
|
||||||
|
"tags": [ {'id': tag.id, 'name': tag.name} for tag in self.tags.all() ]
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<UserStory %s>" % (self.id)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"{0} ({1})".format(self.subject, self.ref)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.id:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
if not self.ref:
|
||||||
|
self.ref = ref_uniquely(self.project, self.__class__)
|
||||||
|
|
||||||
|
super(UserStory, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return ('user-story', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_assign_url(self):
|
||||||
|
return ('assign-us', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_unassign_url(self):
|
||||||
|
return ('unassign-us', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_drop_api_url(self):
|
||||||
|
# TODO: check if this url is used.
|
||||||
|
return ('api:user-story-drop', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_view_url(self):
|
||||||
|
return ('user-story', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
return ('user-story-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_inline_url(self):
|
||||||
|
return ('user-story-edit-inline', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('user-story-delete', (),
|
||||||
|
{'pslug': self.project.slug, 'iref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_task_create_url(self):
|
||||||
|
return ('task-create', (),
|
||||||
|
{'pslug': self.project.slug, 'usref': self.ref})
|
||||||
|
|
||||||
|
""" Propertys """
|
||||||
|
def update_status(self):
|
||||||
|
tasks = self.tasks.all()
|
||||||
|
used_states = []
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
used_states.append(task.fake_status)
|
||||||
|
used_states = set(used_states)
|
||||||
|
|
||||||
|
all_completed = True
|
||||||
|
for state in SCRUM_STATES.ordered_us_states():
|
||||||
|
for task_state in used_states:
|
||||||
|
if task_state == state:
|
||||||
|
self.status = state
|
||||||
|
self.save()
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks_new(self):
|
||||||
|
return self.tasks.filter(status__in=SCRUM_STATES.get_task_states_for_us_state('open'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks_progress(self):
|
||||||
|
return self.tasks.filter(status__in=SCRUM_STATES.get_task_states_for_us_state('progress'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks_completed(self):
|
||||||
|
return self.tasks.filter(status__in=SCRUM_STATES.get_task_states_for_us_state('completed'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks_closed(self):
|
||||||
|
return self.tasks.filter(status__in=SCRUM_STATES.get_task_states_for_us_state('closed'))
|
||||||
|
|
||||||
|
|
||||||
|
class Change(models.Model):
|
||||||
|
change_type = models.IntegerField(choices=TASK_CHANGE_CHOICES)
|
||||||
|
owner = models.ForeignKey('auth.User', related_name='changes')
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
project = models.ForeignKey("Project", related_name="changes")
|
||||||
|
|
||||||
|
content_type = models.ForeignKey(ContentType)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
|
data = DictField()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeAttachment(models.Model):
|
||||||
|
change = models.ForeignKey("Change", related_name="attachments")
|
||||||
|
owner = models.ForeignKey("auth.User", related_name="change_attachments")
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
attached_file = models.FileField(upload_to="files/msg",
|
||||||
|
max_length=500, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskQuerySet(models.query.QuerySet):
|
||||||
|
def _add_categories(self, section_dict, category_id, category_element, selected):
|
||||||
|
section_dict[category_id] = section_dict.get(category_id, {
|
||||||
|
'element': unicode(category_element),
|
||||||
|
'count': 0,
|
||||||
|
'id': category_id,
|
||||||
|
'selected': selected,
|
||||||
|
})
|
||||||
|
section_dict[category_id]['count'] += 1
|
||||||
|
|
||||||
|
def _get_category(self, section_dict, order_by='element', reverse=False):
|
||||||
|
values = section_dict.values()
|
||||||
|
values = sorted(values, key=lambda entry: unicode(entry[order_by]))
|
||||||
|
if reverse:
|
||||||
|
values.reverse()
|
||||||
|
return values
|
||||||
|
|
||||||
|
def _get_filter_and_build_filter_dict(self, queryset, milestone_id, status_id, tags_ids, assigned_to_id, severity_id):
|
||||||
|
task_list = list(queryset)
|
||||||
|
milestones = {}
|
||||||
|
status = {}
|
||||||
|
tags = {}
|
||||||
|
assigned_to = {}
|
||||||
|
severity = {}
|
||||||
|
|
||||||
|
for task in task_list:
|
||||||
|
if task.milestone:
|
||||||
|
selected = milestone_id and task.milestone.id == milestone_id
|
||||||
|
self._add_categories(milestones, task.milestone.id, task.milestone.name, selected)
|
||||||
|
|
||||||
|
selected = status_id and task.status == status_id
|
||||||
|
self._add_categories(status, task.status, task.get_status_display(), selected)
|
||||||
|
|
||||||
|
for tag in task.tags.all():
|
||||||
|
selected = tags_ids and tag.id in tags_ids
|
||||||
|
self._add_categories(tags, tag.id, tag.name, selected)
|
||||||
|
|
||||||
|
if task.assigned_to:
|
||||||
|
selected = assigned_to_id and task.assigned_to.id == assigned_to_id
|
||||||
|
self._add_categories(assigned_to, task.assigned_to.id, task.assigned_to.first_name, selected)
|
||||||
|
|
||||||
|
selected = severity_id and task.severity == int(severity_id)
|
||||||
|
self._add_categories(severity, task.severity, task.get_severity_display(), selected)
|
||||||
|
|
||||||
|
return{
|
||||||
|
'list': task_list,
|
||||||
|
'filters' : {
|
||||||
|
'milestones' : self._get_category(milestones),
|
||||||
|
'status' : self._get_category(status),
|
||||||
|
'tags' : self._get_category(tags),
|
||||||
|
'assigned_to' : self._get_category(assigned_to),
|
||||||
|
'severity' : self._get_category(severity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def filter_and_build_filter_dict(self, milestone=None, status=None, tags=None, assigned_to=None, severity=None):
|
||||||
|
|
||||||
|
queryset = self
|
||||||
|
if milestone:
|
||||||
|
queryset = queryset.filter(milestone = milestone)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
queryset = queryset.filter(status = status)
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
for tag in tags:
|
||||||
|
queryset = queryset.filter(tags__in=[tag])
|
||||||
|
|
||||||
|
if assigned_to:
|
||||||
|
queryset = queryset.filter(assigned_to = assigned_to)
|
||||||
|
|
||||||
|
if severity:
|
||||||
|
queryset = queryset.filter(severity = severity)
|
||||||
|
|
||||||
|
|
||||||
|
milestone_id = milestone and milestone.id
|
||||||
|
status_id = status
|
||||||
|
tags_ids = tags and tags.values_list('id', flat=True)
|
||||||
|
assigned_to_id = assigned_to and assigned_to.id
|
||||||
|
severity_id = severity
|
||||||
|
|
||||||
|
return self._get_filter_and_build_filter_dict(queryset, milestone_id, status_id, tags_ids, assigned_to_id, severity_id)
|
||||||
|
|
||||||
|
class TaskManager(models.Manager):
|
||||||
|
def get_query_set(self):
|
||||||
|
return TaskQuerySet(self.model)
|
||||||
|
|
||||||
|
|
||||||
|
class Task(models.Model):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
||||||
|
user_story = models.ForeignKey('UserStory', related_name='tasks', null=True, blank=True)
|
||||||
|
last_user_story = models.ForeignKey('UserStory', null=True, blank=True)
|
||||||
|
ref = models.CharField(max_length=200, db_index=True, null=True, default=None)
|
||||||
|
status = models.CharField(max_length=50,
|
||||||
|
choices=TASK_STATUS_CHOICES, default='open')
|
||||||
|
owner = models.ForeignKey("auth.User", null=True,
|
||||||
|
default=None, related_name="tasks")
|
||||||
|
|
||||||
|
severity = models.IntegerField(choices=TASK_SEVERITY_CHOICES, default=3)
|
||||||
|
priority = models.IntegerField(choices=TASK_PRIORITY_CHOICES, default=3)
|
||||||
|
milestone = models.ForeignKey('Milestone', related_name='tasks',
|
||||||
|
null=True, default=None, blank=True)
|
||||||
|
|
||||||
|
project = models.ForeignKey('Project', related_name='tasks')
|
||||||
|
type = models.CharField(max_length=10,
|
||||||
|
choices=TASK_TYPE_CHOICES, default='task')
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
finished_date = models.DateTimeField(null=True, blank=True)
|
||||||
|
last_status = models.CharField(max_length=50,
|
||||||
|
choices=TASK_STATUS_CHOICES, null=True, blank=True)
|
||||||
|
|
||||||
|
subject = models.CharField(max_length=500)
|
||||||
|
description = WikiField(blank=True)
|
||||||
|
assigned_to = models.ForeignKey('auth.User',
|
||||||
|
related_name='user_storys_assigned_to_me',
|
||||||
|
blank=True, null=True, default=None)
|
||||||
|
|
||||||
|
watchers = models.ManyToManyField('auth.User',
|
||||||
|
related_name='task_watch', null=True)
|
||||||
|
|
||||||
|
changes = generic.GenericRelation(Change)
|
||||||
|
|
||||||
|
tags = TaggableManager()
|
||||||
|
|
||||||
|
objects = TaskManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('ref', 'project')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.subject
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fake_status(self):
|
||||||
|
return SCRUM_STATES.get_us_state_for_task_state(self.status)
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
if self.type == "bug":
|
||||||
|
return ('issues-view', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
else:
|
||||||
|
return ('tasks-view', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
if self.type == 'bug':
|
||||||
|
return ('issues-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'tref': self.ref})
|
||||||
|
else:
|
||||||
|
#TODO: make this url
|
||||||
|
return ('issues-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'tref': self.ref})
|
||||||
|
#return ('tasks-edit', (),
|
||||||
|
#{'pslug': self.project.slug, 'tref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_view_url(self):
|
||||||
|
if self.type == "bug":
|
||||||
|
return ('issues-view', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
else:
|
||||||
|
return ('tasks-view', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
if self.type == "bug":
|
||||||
|
return ('issues-delete', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
else:
|
||||||
|
return ('tasks-delete', (), {'pslug':self.project.slug, 'tref': self.ref})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
last_user_story = None
|
||||||
|
if self.last_user_story != self.user_story:
|
||||||
|
last_user_story = self.last_user_story
|
||||||
|
self.last_user_story = self.user_story
|
||||||
|
|
||||||
|
if self.id:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
# Store information about close date of a task
|
||||||
|
if self.last_status != self.status:
|
||||||
|
if self.last_status in SCRUM_STATES.get_finished_task_states():
|
||||||
|
if self.status in SCRUM_STATES.get_unfinished_task_states():
|
||||||
|
self.finished_date = None
|
||||||
|
elif self.last_status in SCRUM_STATES.get_unfinished_task_states():
|
||||||
|
if self.status in SCRUM_STATES.get_finished_task_states():
|
||||||
|
self.finished_date = timezone.now()
|
||||||
|
self.last_status = self.status
|
||||||
|
|
||||||
|
if not self.ref:
|
||||||
|
self.ref = ref_uniquely(self.project, self.__class__)
|
||||||
|
|
||||||
|
super(Task, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
if last_user_story:
|
||||||
|
last_user_story.update_status()
|
||||||
|
|
||||||
|
if self.user_story:
|
||||||
|
self.user_story.update_status()
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
self_dict = {
|
||||||
|
'id': self.pk,
|
||||||
|
'editUrl': self.get_edit_url(),
|
||||||
|
'viewUrl': self.get_view_url(),
|
||||||
|
'deleteUrl': self.get_delete_url(),
|
||||||
|
'subject': self.subject,
|
||||||
|
'type': self.get_type_display(),
|
||||||
|
'statusDisplay': self.get_status_display(),
|
||||||
|
'status': self.status,
|
||||||
|
'fakeStatus': self.fake_status,
|
||||||
|
'us': self.user_story and self.user_story.pk or None,
|
||||||
|
'assignedTo': self.assigned_to and self.assigned_to.pk or None,
|
||||||
|
'tags': [tag.to_dict() for tag in self.tags.all()],
|
||||||
|
'priority': self.priority,
|
||||||
|
'priorityDisplay': self.get_priority_display(),
|
||||||
|
'severity': self.severity,
|
||||||
|
'severityDisplay': self.get_severity_display(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self_dict['assignedTo']:
|
||||||
|
self_dict['assignedToDisplay'] = self.assigned_to.get_full_name()
|
||||||
|
else:
|
||||||
|
self_dict['assignedToDisplay'] = ugettext("Unassigned")
|
||||||
|
return self_dict
|
||||||
|
|
||||||
|
|
||||||
|
reversion.register(ProjectExtras)
|
||||||
|
reversion.register(Project)
|
||||||
|
reversion.register(ProjectUserRole)
|
||||||
|
reversion.register(Milestone)
|
||||||
|
reversion.register(UserStory)
|
||||||
|
reversion.register(Change)
|
||||||
|
reversion.register(ChangeAttachment)
|
||||||
|
reversion.register(Task)
|
||||||
|
|
||||||
|
from . import sigdispatch
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
from haystack import indexes
|
||||||
|
from .models import Project, Milestone, UserStory, Task
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/userstory_text.txt')
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return UserStory
|
||||||
|
|
||||||
|
def index_queryset(self):
|
||||||
|
return self.get_model().objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/task_text.txt')
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return Task
|
||||||
|
|
||||||
|
def index_queryset(self):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,121 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from greenmine.profile.models import Profile
|
||||||
|
from greenmine.scrum.models import UserStory, Task, ProjectUserRole
|
||||||
|
from greenmine.core.utils import normalize_tagname
|
||||||
|
from greenmine.core import signals
|
||||||
|
from greenmine.core.utils.auth import set_token
|
||||||
|
|
||||||
|
from greenqueue import send_task
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
@receiver(signals.mail_new_user)
|
||||||
|
def mail_new_user(sender, user, **kwargs):
|
||||||
|
template = render_to_string("email/new.user.html", {
|
||||||
|
"user": user,
|
||||||
|
"token": set_token(user),
|
||||||
|
'current_host': settings.HOST,
|
||||||
|
})
|
||||||
|
|
||||||
|
subject = ugettext("Greenmine: wellcome!")
|
||||||
|
send_task("send-mail", args = [subject, template, [user.email]])
|
||||||
|
|
||||||
|
@receiver(signals.mail_recovery_password)
|
||||||
|
def mail_recovery_password(sender, user, **kwargs):
|
||||||
|
template = render_to_string("email/forgot.password.html", {
|
||||||
|
"user": user,
|
||||||
|
"token": set_token(user),
|
||||||
|
"current_host": settings.HOST,
|
||||||
|
})
|
||||||
|
subject = ugettext("Greenmine: password recovery.")
|
||||||
|
send_task("send-mail", args = [subject, template, [user.email]])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.mail_milestone_created)
|
||||||
|
def mail_milestone_created(sender, milestone, user, **kwargs):
|
||||||
|
participants_ids = ProjectUserRole.objects\
|
||||||
|
.filter(user=user, mail_milestone_created=True, project=milestone.project)\
|
||||||
|
.values_list('user__pk', flat=True)
|
||||||
|
|
||||||
|
participants = User.objects.filter(pk__in=participants_ids)
|
||||||
|
|
||||||
|
emails_list = []
|
||||||
|
subject = ugettext("Greenmine: sprint created")
|
||||||
|
for person in participants:
|
||||||
|
template = render_to_string("email/milestone.created.html", {
|
||||||
|
"person": person,
|
||||||
|
"current_host": settings.HOST,
|
||||||
|
"milestone": milestone,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
|
||||||
|
emails_list.append([subject, template, [person.email]])
|
||||||
|
|
||||||
|
send_task("send-bulk-mail", args=[emails_list])
|
||||||
|
|
||||||
|
@receiver(signals.mail_userstory_created)
|
||||||
|
def mail_userstory_created(sender, us, user, **kwargs):
|
||||||
|
participants_ids = ProjectUserRole.objects\
|
||||||
|
.filter(user=user, mail_userstory_created=True, project=us.project)\
|
||||||
|
.values_list('user__pk', flat=True)
|
||||||
|
|
||||||
|
participants = User.objects.filter(pk__in=participants_ids)
|
||||||
|
|
||||||
|
emails_list = []
|
||||||
|
subject = ugettext("Greenmine: user story created")
|
||||||
|
|
||||||
|
for person in participants:
|
||||||
|
template = render_to_string("email/userstory.created.html", {
|
||||||
|
"person": person,
|
||||||
|
"current_host": settings.HOST,
|
||||||
|
"us": us,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
|
||||||
|
emails_list.append([subject, template, [person.email]])
|
||||||
|
|
||||||
|
send_task("send-bulk-mail", args=[emails_list])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.mail_task_created)
|
||||||
|
def mail_task_created(sender, task, user, **kwargs):
|
||||||
|
participants_ids = ProjectUserRole.objects\
|
||||||
|
.filter(user=user, mail_task_created=True, project=task.project)\
|
||||||
|
.values_list('user__pk', flat=True)
|
||||||
|
|
||||||
|
participants = User.objects.filter(pk__in=participants_ids)
|
||||||
|
|
||||||
|
emails_list = []
|
||||||
|
subject = ugettext("Greenmine: task created")
|
||||||
|
|
||||||
|
for person in participants:
|
||||||
|
template = render_to_string("email/task.created.html", {
|
||||||
|
"person": person,
|
||||||
|
"current_host": settings.HOST,
|
||||||
|
"task": task,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
|
||||||
|
emails_list.append([subject, template, [person.email]])
|
||||||
|
|
||||||
|
send_task("send-bulk-mail", args=[emails_list])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.mail_task_assigned)
|
||||||
|
def mail_task_assigned(sender, task, user, **kwargs):
|
||||||
|
template = render_to_string("email/task.assigned.html", {
|
||||||
|
"person": task.assigned_to,
|
||||||
|
"task": task,
|
||||||
|
"user": user,
|
||||||
|
"current_host": settings.HOST,
|
||||||
|
})
|
||||||
|
|
||||||
|
subject = ugettext("Greenmine: task assigned")
|
||||||
|
send_task("send-mail", args = [subject, template, [task.assigned_to.email]])
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{ object.uuid }}
|
||||||
|
{{ object.user_story }}
|
||||||
|
{{ object.ref }}
|
||||||
|
{{ object.status }}
|
||||||
|
{{ object.owner }}
|
||||||
|
{{ object.milestone }}
|
||||||
|
{{ object.project }}
|
||||||
|
{{ object.type }}
|
||||||
|
{{ object.created_date }}
|
||||||
|
{{ object.modified_date }}
|
||||||
|
{{ object.finished_date }}
|
||||||
|
{{ object.last_status }}
|
||||||
|
{{ object.subject }}
|
||||||
|
{{ object.description }}
|
||||||
|
{{ object.assigned_to }}
|
||||||
|
{% for watcher in object.watchers.all %}
|
||||||
|
{{ watcher }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for tag in object.tags.all %}
|
||||||
|
{{ tag }}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{ object.uuid }}
|
||||||
|
{{ object.ref }}
|
||||||
|
{{ object.milestone }}
|
||||||
|
{{ object.project }}
|
||||||
|
{{ object.owner }}
|
||||||
|
{{ object.status }}
|
||||||
|
{% for tag in object.tags.all %}
|
||||||
|
{{ tag }}
|
||||||
|
{% endfor %}
|
||||||
|
{{ object.created_date }}
|
||||||
|
{{ object.modified_date }}
|
||||||
|
{{ object.subject }}
|
||||||
|
{{ object.description }}
|
||||||
|
{{ object.finish_date }}
|
||||||
|
{% for watcher in object.watchers.all %}
|
||||||
|
{{ watcher }}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,64 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
__all__ = ('SCRUM_STATES',)
|
||||||
|
|
||||||
|
class GmScrumStates(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._states = settings.GM_SCRUM_STATES
|
||||||
|
|
||||||
|
def get_task_choices(self):
|
||||||
|
task_choices = []
|
||||||
|
for us_state in self._states.values():
|
||||||
|
task_choices += us_state['task_states']
|
||||||
|
return task_choices
|
||||||
|
|
||||||
|
def get_us_choices(self):
|
||||||
|
us_choices = []
|
||||||
|
for key, value in self._states.iteritems():
|
||||||
|
us_choices.append((key, value['name']))
|
||||||
|
return us_choices
|
||||||
|
|
||||||
|
def get_finished_task_states(self):
|
||||||
|
finished_task_states = []
|
||||||
|
for us_state in self._states.values():
|
||||||
|
if us_state['is_finished']:
|
||||||
|
finished_task_states += us_state['task_states']
|
||||||
|
return [ x[0] for x in finished_task_states ]
|
||||||
|
|
||||||
|
def get_unfinished_task_states(self):
|
||||||
|
unfinished_task_states = []
|
||||||
|
for us_state in self._states.values():
|
||||||
|
if not us_state['is_finished']:
|
||||||
|
unfinished_task_states += us_state['task_states']
|
||||||
|
return [ x[0] for x in unfinished_task_states ]
|
||||||
|
|
||||||
|
def get_finished_us_states(self):
|
||||||
|
finished_us_states = []
|
||||||
|
for key, value in self._states.iteritems():
|
||||||
|
if value['is_finished']:
|
||||||
|
finished_us_states.append(key)
|
||||||
|
return finished_us_states
|
||||||
|
|
||||||
|
def get_unfinished_us_states(self):
|
||||||
|
finished_us_states = []
|
||||||
|
for key, value in self._states.iteritems():
|
||||||
|
if not value['is_finished']:
|
||||||
|
finished_us_states.append(key)
|
||||||
|
return finished_us_states
|
||||||
|
|
||||||
|
def get_us_state_for_task_state(self, state):
|
||||||
|
for key, value in self._states.iteritems():
|
||||||
|
if state in [ x[0] for x in value['task_states'] ]:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_task_states_for_us_state(self, state):
|
||||||
|
if state in self._states.keys():
|
||||||
|
return [ x[0] for x in self._states[state]['task_states'] ]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ordered_us_states(self):
|
||||||
|
ordered = sorted([ (value['order'], key) for key, value in self._states.iteritems() ])
|
||||||
|
return [ x[1] for x in ordered ]
|
||||||
|
|
||||||
|
SCRUM_STATES = GmScrumStates()
|
|
@ -0,0 +1,17 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import os
|
||||||
|
|
||||||
|
if "GREENMINE_ENVIRON" in os.environ:
|
||||||
|
if os.environ["GREENMINE_ENVIRON"] in ('production', 'development', 'local'):
|
||||||
|
print "importing %s" % os.environ["GREENMINE_ENVIRON"]
|
||||||
|
eval("from .%s import *" % (os.environ["GREENMINE_ENVIRON"]))
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
print "Trying import local.py settings..."
|
||||||
|
from .local import *
|
||||||
|
except ImportError:
|
||||||
|
print "Trying import development.py settings..."
|
||||||
|
from .development import *
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
GM_SCRUM_STATES = {
|
||||||
|
'open': {
|
||||||
|
'name': 'Open',
|
||||||
|
'is_finished': False,
|
||||||
|
'order': 10,
|
||||||
|
'task_states': [
|
||||||
|
('open', 'Open'),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'progress': {
|
||||||
|
'name': 'In progress',
|
||||||
|
'is_finished': False,
|
||||||
|
'order': 20,
|
||||||
|
'task_states': [
|
||||||
|
('progress', 'In progress'),
|
||||||
|
('needs_info', 'Needs info'),
|
||||||
|
('posponed', 'Posponed'),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'completed': {
|
||||||
|
'name': 'Ready for test',
|
||||||
|
'is_finished': True,
|
||||||
|
'order': 30,
|
||||||
|
'task_states': [
|
||||||
|
('completed', 'Ready for test'),
|
||||||
|
('workaround', 'Workaround'),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'closed': {
|
||||||
|
'name': 'Closed',
|
||||||
|
'is_finished': True,
|
||||||
|
'order': 40,
|
||||||
|
'task_states': [
|
||||||
|
('closed', 'Closed'),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,320 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
import os.path, sys, os
|
||||||
|
|
||||||
|
PROJECT_ROOT = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
|
||||||
|
)
|
||||||
|
|
||||||
|
OUT_PROJECT_ROOT = os.path.abspath(
|
||||||
|
os.path.join(PROJECT_ROOT, "..")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
LOGS_PATH = os.path.join(OUT_PROJECT_ROOT, 'logs')
|
||||||
|
BACKUP_PATH = os.path.join(OUT_PROJECT_ROOT, 'exports')
|
||||||
|
|
||||||
|
if not os.path.exists(LOGS_PATH):
|
||||||
|
os.mkdir(LOGS_PATH)
|
||||||
|
|
||||||
|
if not os.path.exists(BACKUP_PATH):
|
||||||
|
os.mkdir(BACKUP_PATH)
|
||||||
|
|
||||||
|
ADMINS = (
|
||||||
|
('Andrei Antoukh', 'niwi@niwi.be'),
|
||||||
|
)
|
||||||
|
|
||||||
|
LANGUAGES = (
|
||||||
|
('es', _('Spanish')),
|
||||||
|
('en', _('English')),
|
||||||
|
('ru', _('Russian')),
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'test' in sys.argv:
|
||||||
|
if "settings" not in ",".join(sys.argv):
|
||||||
|
print ("Not settings specified. \nTry: python manage.py test "
|
||||||
|
"--settings=greenmine.settings.testing -v2 scrum")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
|
DISABLE_REGISTRATION = False
|
||||||
|
DEFAULT_TASK_PARSER_RE = "^\s*Task\:(.+)$"
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||||
|
'NAME': os.path.join(OUT_PROJECT_ROOT, 'database.sqlite'), # Or path to database file if using sqlite3.
|
||||||
|
'OPTIONS': {'timeout': 20}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# CACHE CONFIG
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
'LOCATION': 'unique-snowflake'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PASSWORD_HASHERS = [
|
||||||
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||||
|
]
|
||||||
|
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
|
||||||
|
SEND_BROKEN_LINK_EMAILS = True
|
||||||
|
IGNORABLE_404_ENDS = ('.php', '.cgi')
|
||||||
|
IGNORABLE_404_STARTS = ('/phpmyadmin/',)
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Madrid'
|
||||||
|
LANGUAGE_CODE = 'en'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
LOGIN_URL='/auth/login/'
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
#SESSION BACKEND
|
||||||
|
#SESSION_ENGINE='django.contrib.sessions.backends.db'
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.cache'
|
||||||
|
#SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||||
|
#SESSION_SAVE_EVERY_REQUEST = True
|
||||||
|
SESSION_COOKIE_AGE = 1209600 # (2 weeks)
|
||||||
|
|
||||||
|
HOST = 'http://localhost:8000'
|
||||||
|
|
||||||
|
# MAIL OPTIONS
|
||||||
|
#EMAIL_USE_TLS = False
|
||||||
|
#EMAIL_HOST = 'localhost'
|
||||||
|
#EMAIL_HOST_USER = 'user'
|
||||||
|
#EMAIL_HOST_PASSWORD = 'password'
|
||||||
|
#EMAIL_PORT = 25
|
||||||
|
DEFAULT_FROM_EMAIL = "niwi@niwi.be"
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
#EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
|
|
||||||
|
GREENQUEUE_BACKEND = 'greenqueue.backends.sync.SyncService'
|
||||||
|
GREENQUEUE_WORKER_MANAGER = 'greenqueue.worker.sync.SyncManager'
|
||||||
|
|
||||||
|
GREENQUEUE_TASK_MODULES = [
|
||||||
|
'greenmine.core.mail.async_tasks',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
SV_CSS_MENU_ACTIVE = 'selected'
|
||||||
|
SV_CONTEXT_VARNAME = 'menu'
|
||||||
|
|
||||||
|
# Message System
|
||||||
|
#MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
||||||
|
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||||
|
|
||||||
|
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||||
|
# Example: "/home/media/media.lawrence.com/media/"
|
||||||
|
MEDIA_ROOT = os.path.join(OUT_PROJECT_ROOT, 'media')
|
||||||
|
|
||||||
|
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||||
|
# trailing slash.
|
||||||
|
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
# Absolute path to the directory static files should be collected to.
|
||||||
|
# Don't put anything in this directory yourself; store your static files
|
||||||
|
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||||
|
# Example: "/home/media/media.lawrence.com/static/"
|
||||||
|
STATIC_ROOT = os.path.join(OUT_PROJECT_ROOT, 'static')
|
||||||
|
|
||||||
|
# URL prefix for static files.
|
||||||
|
# Example: "http://media.lawrence.com/static/"
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||||
|
# Make sure to use a trailing slash.
|
||||||
|
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||||
|
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||||
|
|
||||||
|
|
||||||
|
# Additional locations of static files
|
||||||
|
STATICFILES_DIRS = (
|
||||||
|
# Put strings here, like "/home/html/static" or "C:/www/django/static".
|
||||||
|
# Don't forget to use absolute paths, not relative paths.
|
||||||
|
)
|
||||||
|
|
||||||
|
LOCALE_PATHS = (
|
||||||
|
os.path.join(PROJECT_ROOT, 'locale'),
|
||||||
|
)
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
]
|
||||||
|
|
||||||
|
SECRET_KEY = 'aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e'
|
||||||
|
|
||||||
|
TEMPLATE_LOADERS = [
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = [
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'greenmine.core.middleware.PermissionMiddleware',
|
||||||
|
'django.middleware.transaction.TransactionMiddleware',
|
||||||
|
'reversion.middleware.RevisionMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
TEMPLATE_CONTEXT_PROCESSORS = [
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.core.context_processors.debug",
|
||||||
|
"django.core.context_processors.i18n",
|
||||||
|
"django.core.context_processors.media",
|
||||||
|
"django.core.context_processors.static",
|
||||||
|
"django.core.context_processors.tz",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"greenmine.core.context.main",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'greenmine.urls'
|
||||||
|
|
||||||
|
TEMPLATE_DIRS = [
|
||||||
|
os.path.join(PROJECT_ROOT, "templates"),
|
||||||
|
]
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# Django base applications
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
'greenmine.base',
|
||||||
|
'greenmine.profile',
|
||||||
|
'greenmine.scrum',
|
||||||
|
'greenmine.wiki',
|
||||||
|
'greenmine.documents',
|
||||||
|
'greenmine.taggit',
|
||||||
|
'greenmine.questions',
|
||||||
|
'greenmine.search',
|
||||||
|
|
||||||
|
'django_gravatar',
|
||||||
|
'rawinclude',
|
||||||
|
'greenqueue',
|
||||||
|
'south',
|
||||||
|
'superview',
|
||||||
|
'haystack',
|
||||||
|
'reversion',
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'greenmine.wsgi.application'
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': True,
|
||||||
|
'filters': {
|
||||||
|
'require_debug_false': {
|
||||||
|
'()': 'django.utils.log.RequireDebugFalse'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'formatters': {
|
||||||
|
'simple': {
|
||||||
|
'format': '%(levelname)s:%(asctime)s:%(module)s %(message)s'
|
||||||
|
},
|
||||||
|
'null': {
|
||||||
|
'format': '%(message)s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'null': {
|
||||||
|
'level':'DEBUG',
|
||||||
|
'class':'django.utils.log.NullHandler',
|
||||||
|
},
|
||||||
|
'fileout': {
|
||||||
|
'level':'DEBUG',
|
||||||
|
'class':'logging.FileHandler',
|
||||||
|
'filename': os.path.join(LOGS_PATH, 'greenmine.log'),
|
||||||
|
'formatter': 'simple',
|
||||||
|
},
|
||||||
|
'queryhandler': {
|
||||||
|
'level':'DEBUG',
|
||||||
|
'class':'logging.FileHandler',
|
||||||
|
'filename': os.path.join(LOGS_PATH, 'greenmine-querys.log'),
|
||||||
|
},
|
||||||
|
'console':{
|
||||||
|
'level':'DEBUG',
|
||||||
|
'class':'logging.StreamHandler',
|
||||||
|
'formatter': 'null',
|
||||||
|
},
|
||||||
|
'mail_admins': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'filters': ['require_debug_false'],
|
||||||
|
'class': 'django.utils.log.AdminEmailHandler',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers':['null'],
|
||||||
|
'propagate': True,
|
||||||
|
'level':'INFO',
|
||||||
|
},
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['mail_admins', 'console'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'django.db.backends':{
|
||||||
|
'handlers': ['queryhandler'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'main': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'asyncmail': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level':'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'greenqueue': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_PROFILE_MODULE = 'profile.Profile'
|
||||||
|
FORMAT_MODULE_PATH = 'greenmine.core.formats'
|
||||||
|
DATE_INPUT_FORMATS = (
|
||||||
|
'%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%b %d %Y',
|
||||||
|
'%b %d, %Y', '%d %b %Y', '%d %b, %Y', '%B %d %Y',
|
||||||
|
'%B %d, %Y', '%d %B %Y', '%d %B, %Y'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HAYSTACK_CONNECTIONS = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
|
||||||
|
'PATH': os.path.join(os.path.dirname(__file__), '../search/index'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
HAYSTACK_DEFAULT_OPERATOR = 'AND'
|
||||||
|
|
||||||
|
|
||||||
|
from .appdefaults import *
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .common import *
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
USE_ETAGS = False
|
||||||
|
|
||||||
|
SESSION_ENGINE='django.contrib.sessions.backends.db'
|
||||||
|
|
||||||
|
TEMPLATE_CONTEXT_PROCESSORS += [
|
||||||
|
"django.core.context_processors.debug",
|
||||||
|
]
|
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .development import *
|
||||||
|
|
||||||
|
#DATABASES = {
|
||||||
|
# 'default': {
|
||||||
|
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
# 'NAME': 'greenmine',
|
||||||
|
# 'USER': 'greenmine',
|
||||||
|
# 'PASSWORD': '',
|
||||||
|
# 'HOST': '',
|
||||||
|
# 'PORT': '',
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#HOST="http://greenmine.projects.kaleidos.net"
|
||||||
|
#
|
||||||
|
#MEDIA_ROOT = '/home/greenmine/media'
|
||||||
|
#STATIC_ROOT = '/home/greenmine/static'
|
||||||
|
|
||||||
|
#EMAIL_USE_TLS = False
|
||||||
|
#EMAIL_HOST = 'localhost'
|
||||||
|
#EMAIL_HOST_USER = 'user'
|
||||||
|
#EMAIL_HOST_PASSWORD = 'password'
|
||||||
|
#EMAIL_PORT = 25
|
||||||
|
#DEFAULT_FROM_EMAIL = "niwi@niwi.be"
|
||||||
|
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
|
||||||
|
# GMAIL SETTINGS EXAMPLE
|
||||||
|
#EMAIL_USE_TLS = True
|
||||||
|
#EMAIL_HOST = 'smtp.gmail.com'
|
||||||
|
#EMAIL_HOST_USER = 'youremail@gmail.com'
|
||||||
|
#EMAIL_HOST_PASSWORD = 'yourpassword'
|
||||||
|
#EMAIL_PORT = 587
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .common import *
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
USE_ETAGS = True
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES += [
|
||||||
|
'django.middleware.http.ConditionalGetMiddleware',
|
||||||
|
'django.middleware.gzip.GZipMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
LOGGING['loggers']['django.db.backends']['handlers'] = ['null']
|
||||||
|
LOGGING['loggers']['django.request']['handlers'] = ['mail_admins']
|
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .development import *
|
||||||
|
|
||||||
|
GREENQUEUE_BACKEND = 'greenqueue.backends.sync.SyncService'
|
||||||
|
GREENQUEUE_WORKER_MANAGER = 'greenqueue.worker.sync.SyncManager'
|
||||||
|
|
||||||
|
INSTALLED_APPS.append('greenmine.taggit.tests')
|
|
@ -0,0 +1 @@
|
||||||
|
VERSION = (0, 9, 3)
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Tag, TaggedItem
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemInline(admin.StackedInline):
|
||||||
|
model = TaggedItem
|
||||||
|
|
||||||
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name"]
|
||||||
|
inlines = [
|
||||||
|
TaggedItemInline
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Tag, TagAdmin)
|
|
@ -0,0 +1,67 @@
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: django-taggit\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2010-09-07 09:26-0700\n"
|
||||||
|
"PO-Revision-Date: 2010-09-07 09:26-0700\n"
|
||||||
|
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
|
||||||
|
"Language-Team: German <de@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||||
|
|
||||||
|
#: forms.py:20
|
||||||
|
msgid "Please provide a comma-separated list of tags."
|
||||||
|
msgstr "Bitte eine durch Komma getrennte Schlagwortliste eingeben."
|
||||||
|
|
||||||
|
#: managers.py:39 managers.py:83 models.py:50
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "Schlagwörter"
|
||||||
|
|
||||||
|
#: managers.py:84
|
||||||
|
msgid "A comma-separated list of tags."
|
||||||
|
msgstr "Eine durch Komma getrennte Schlagwortliste."
|
||||||
|
|
||||||
|
#: models.py:10
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
#: models.py:11
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr "Kürzel"
|
||||||
|
|
||||||
|
#: models.py:49
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr "Schlagwort"
|
||||||
|
|
||||||
|
#: models.py:56
|
||||||
|
#, python-format
|
||||||
|
msgid "%(object)s tagged with %(tag)s"
|
||||||
|
msgstr "%(object)s verschlagwortet mit %(tag)s"
|
||||||
|
|
||||||
|
#: models.py:100
|
||||||
|
msgid "Object id"
|
||||||
|
msgstr "Objekt-ID"
|
||||||
|
|
||||||
|
#: models.py:104 models.py:110
|
||||||
|
msgid "Content type"
|
||||||
|
msgstr "Inhaltstyp"
|
||||||
|
|
||||||
|
#: models.py:138
|
||||||
|
msgid "Tagged Item"
|
||||||
|
msgstr "Verschlagwortetes Objekt"
|
||||||
|
|
||||||
|
#: models.py:139
|
||||||
|
msgid "Tagged Items"
|
||||||
|
msgstr "Verschlagwortete Objekte"
|
||||||
|
|
||||||
|
#: contrib/suggest/models.py:57
|
||||||
|
msgid ""
|
||||||
|
"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
|
||||||
|
"\" in your expression."
|
||||||
|
msgstr ""
|
||||||
|
"Bitte einen regulären Ausdruck eingeben. Fügen Sie \"(?i) \" dem "
|
||||||
|
"Ausdruck hinzu, um nicht zwischen Groß- und Kleinschreibung zu "
|
||||||
|
"unterscheiden."
|
|
@ -0,0 +1,68 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2010-09-07 09:45-0700\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: forms.py:20
|
||||||
|
msgid "Please provide a comma-separated list of tags."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: managers.py:39 managers.py:83 models.py:50
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: managers.py:84
|
||||||
|
msgid "A comma-separated list of tags."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:10
|
||||||
|
msgid "Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:11
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:49
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:56
|
||||||
|
#, python-format
|
||||||
|
msgid "%(object)s tagged with %(tag)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:100
|
||||||
|
msgid "Object id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:104 models.py:110
|
||||||
|
msgid "Content type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:138
|
||||||
|
msgid "Tagged Item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:139
|
||||||
|
msgid "Tagged Items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: contrib/suggest/models.py:57
|
||||||
|
msgid ""
|
||||||
|
"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
|
||||||
|
"\" in your expression."
|
||||||
|
msgstr ""
|
|
@ -0,0 +1,69 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Django Taggit\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2010-06-26 12:47-0500\n"
|
||||||
|
"PO-Revision-Date: 2010-06-26 12:54-0600\n"
|
||||||
|
"Last-Translator: Alex <alex.gaynor@gmail.com>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: forms.py:20
|
||||||
|
msgid "Please provide a comma-separated list of tags."
|
||||||
|
msgstr "נא לספק רשימה של תגים מופרדת עם פסיקים."
|
||||||
|
|
||||||
|
#: managers.py:41
|
||||||
|
#: managers.py:113
|
||||||
|
#: models.py:18
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "תגיות"
|
||||||
|
|
||||||
|
#: managers.py:114
|
||||||
|
msgid "A comma-separated list of tags."
|
||||||
|
msgstr "רשימה של תגים מופרדת עם פסיקים."
|
||||||
|
|
||||||
|
#: models.py:10
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "שם"
|
||||||
|
|
||||||
|
#: models.py:11
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:17
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr "תג"
|
||||||
|
|
||||||
|
#: models.py:56
|
||||||
|
#, python-format
|
||||||
|
msgid "%(object)s tagged with %(tag)s"
|
||||||
|
msgstr "%(object)s מתויג עם %(tag)s"
|
||||||
|
|
||||||
|
#: models.py:86
|
||||||
|
msgid "Object id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:87
|
||||||
|
msgid "Content type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:92
|
||||||
|
msgid "Tagged Item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:93
|
||||||
|
msgid "Tagged Items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: contrib/suggest/models.py:57
|
||||||
|
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: django-taggit\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2010-09-07 09:45-0700\n"
|
||||||
|
"PO-Revision-Date: 2010-09-07 23:04+0100\n"
|
||||||
|
"Last-Translator: Jeffrey Gelens <jeffrey@gelens.org>\n"
|
||||||
|
"Language-Team: Dutch\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: forms.py:20
|
||||||
|
msgid "Please provide a comma-separated list of tags."
|
||||||
|
msgstr "Geef een door komma gescheiden lijst van tags."
|
||||||
|
|
||||||
|
#: managers.py:39
|
||||||
|
#: managers.py:83
|
||||||
|
#: models.py:50
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "Tags"
|
||||||
|
|
||||||
|
#: managers.py:84
|
||||||
|
msgid "A comma-separated list of tags."
|
||||||
|
msgstr "Een door komma gescheiden lijst van tags."
|
||||||
|
|
||||||
|
#: models.py:10
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Naam"
|
||||||
|
|
||||||
|
#: models.py:11
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr "Slug"
|
||||||
|
|
||||||
|
#: models.py:49
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr "Tag"
|
||||||
|
|
||||||
|
#: models.py:56
|
||||||
|
#, python-format
|
||||||
|
msgid "%(object)s tagged with %(tag)s"
|
||||||
|
msgstr "%(object)s getagged met %(tag)s"
|
||||||
|
|
||||||
|
#: models.py:100
|
||||||
|
msgid "Object id"
|
||||||
|
msgstr "Object-id"
|
||||||
|
|
||||||
|
#: models.py:104
|
||||||
|
#: models.py:110
|
||||||
|
msgid "Content type"
|
||||||
|
msgstr "Inhoudstype"
|
||||||
|
|
||||||
|
#: models.py:138
|
||||||
|
msgid "Tagged Item"
|
||||||
|
msgstr "Object getagged"
|
||||||
|
|
||||||
|
#: models.py:139
|
||||||
|
msgid "Tagged Items"
|
||||||
|
msgstr "Objecten getagged"
|
||||||
|
|
||||||
|
#: contrib/suggest/models.py:57
|
||||||
|
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
|
||||||
|
msgstr "Voer een valide reguliere expressie in. Voeg \"(?i)\" aan de expressie toe om deze hoofdletter ongevoelig te maken."
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Django Taggit\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2010-06-11 11:28+0700\n"
|
||||||
|
"PO-Revision-Date: 2010-06-11 11:30+0700\n"
|
||||||
|
"Last-Translator: Igor 'idle sign' Starikov <idlesign@yandex.ru>\n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||||
|
"X-Poedit-Language: Russian\n"
|
||||||
|
|
||||||
|
#: forms.py:20
|
||||||
|
msgid "Please provide a comma-separated list of tags."
|
||||||
|
msgstr "Укажите метки через запятую."
|
||||||
|
|
||||||
|
#: managers.py:41
|
||||||
|
#: managers.py:101
|
||||||
|
#: models.py:17
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "Метки"
|
||||||
|
|
||||||
|
#: managers.py:102
|
||||||
|
msgid "A comma-separated list of tags."
|
||||||
|
msgstr "Список меток через запятую."
|
||||||
|
|
||||||
|
#: models.py:9
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Название"
|
||||||
|
|
||||||
|
#: models.py:10
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr "Слаг"
|
||||||
|
|
||||||
|
#: models.py:16
|
||||||
|
msgid "Tag"
|
||||||
|
msgstr "Метка"
|
||||||
|
|
||||||
|
#: models.py:55
|
||||||
|
#, python-format
|
||||||
|
msgid "%(object)s tagged with %(tag)s"
|
||||||
|
msgstr "элемент «%(object)s» с меткой «%(tag)s»"
|
||||||
|
|
||||||
|
#: models.py:82
|
||||||
|
msgid "Object id"
|
||||||
|
msgstr "ID объекта"
|
||||||
|
|
||||||
|
#: models.py:83
|
||||||
|
msgid "Content type"
|
||||||
|
msgstr "Тип содержимого"
|
||||||
|
|
||||||
|
#: models.py:87
|
||||||
|
msgid "Tagged Item"
|
||||||
|
msgstr "Элемент с меткой"
|
||||||
|
|
||||||
|
#: models.py:88
|
||||||
|
msgid "Tagged Items"
|
||||||
|
msgstr "Элементы с меткой"
|
||||||
|
|
||||||
|
#: contrib/suggest/models.py:57
|
||||||
|
msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
|
||||||
|
msgstr "Введите регулярное выражение. Чтобы сделать его чувствительным к регистру укажите \"(?i)\"."
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.generic import GenericRelation
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields.related import ManyToManyRel, RelatedField, add_lazy_relation
|
||||||
|
from django.db.models.related import RelatedObject
|
||||||
|
from django.utils.text import capfirst
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .forms import TagField
|
||||||
|
from .models import TaggedItem, GenericTaggedItemBase, Tag
|
||||||
|
from .utils import require_instance_manager
|
||||||
|
|
||||||
|
|
||||||
|
class TaggableRel(ManyToManyRel):
|
||||||
|
def __init__(self):
|
||||||
|
self.related_name = None
|
||||||
|
self.limit_choices_to = {}
|
||||||
|
self.symmetrical = True
|
||||||
|
self.multiple = True
|
||||||
|
self.through = None
|
||||||
|
|
||||||
|
|
||||||
|
class TaggableManager(RelatedField):
|
||||||
|
def __init__(self, verbose_name=_("Tags"),
|
||||||
|
help_text=_("A comma-separated list of tags."), through=None, blank=False):
|
||||||
|
self.through = through or TaggedItem
|
||||||
|
self.rel = TaggableRel()
|
||||||
|
self.verbose_name = verbose_name
|
||||||
|
self.help_text = help_text
|
||||||
|
self.blank = blank
|
||||||
|
self.editable = True
|
||||||
|
self.unique = False
|
||||||
|
self.creates_table = False
|
||||||
|
self.db_column = None
|
||||||
|
self.choices = None
|
||||||
|
self.serialize = False
|
||||||
|
self.null = True
|
||||||
|
self.creation_counter = models.Field.creation_counter
|
||||||
|
models.Field.creation_counter += 1
|
||||||
|
|
||||||
|
def __get__(self, instance, model):
|
||||||
|
if instance is not None and instance.pk is None:
|
||||||
|
raise ValueError("%s objects need to have a primary key value "
|
||||||
|
"before you can access their tags." % model.__name__)
|
||||||
|
manager = _TaggableManager(
|
||||||
|
through=self.through, model=model, instance=instance
|
||||||
|
)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name):
|
||||||
|
self.name = self.column = name
|
||||||
|
self.model = cls
|
||||||
|
cls._meta.add_field(self)
|
||||||
|
setattr(cls, name, self)
|
||||||
|
if not cls._meta.abstract:
|
||||||
|
if isinstance(self.through, basestring):
|
||||||
|
def resolve_related_class(field, model, cls):
|
||||||
|
self.through = model
|
||||||
|
self.post_through_setup(cls)
|
||||||
|
add_lazy_relation(
|
||||||
|
cls, self, self.through, resolve_related_class
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.post_through_setup(cls)
|
||||||
|
|
||||||
|
def post_through_setup(self, cls):
|
||||||
|
self.use_gfk = (
|
||||||
|
self.through is None or issubclass(self.through, GenericTaggedItemBase)
|
||||||
|
)
|
||||||
|
self.rel.to = self.through._meta.get_field("tag").rel.to
|
||||||
|
if self.use_gfk:
|
||||||
|
tagged_items = GenericRelation(self.through)
|
||||||
|
tagged_items.contribute_to_class(cls, "tagged_items")
|
||||||
|
|
||||||
|
def save_form_data(self, instance, value):
|
||||||
|
getattr(instance, self.name).set(*value)
|
||||||
|
|
||||||
|
def formfield(self, form_class=TagField, **kwargs):
|
||||||
|
defaults = {
|
||||||
|
"label": capfirst(self.verbose_name),
|
||||||
|
"help_text": self.help_text,
|
||||||
|
"required": not self.blank
|
||||||
|
}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return form_class(**defaults)
|
||||||
|
|
||||||
|
def value_from_object(self, instance):
|
||||||
|
if instance.pk:
|
||||||
|
return self.through.objects.filter(**self.through.lookup_kwargs(instance))
|
||||||
|
return self.through.objects.none()
|
||||||
|
|
||||||
|
def related_query_name(self):
|
||||||
|
return self.model._meta.module_name
|
||||||
|
|
||||||
|
def m2m_reverse_name(self):
|
||||||
|
return self.through._meta.get_field_by_name("tag")[0].column
|
||||||
|
|
||||||
|
def m2m_target_field_name(self):
|
||||||
|
return self.model._meta.pk.name
|
||||||
|
|
||||||
|
def m2m_reverse_target_field_name(self):
|
||||||
|
return self.rel.to._meta.pk.name
|
||||||
|
|
||||||
|
def m2m_column_name(self):
|
||||||
|
if self.use_gfk:
|
||||||
|
return self.through._meta.virtual_fields[0].fk_field
|
||||||
|
return self.through._meta.get_field('content_object').column
|
||||||
|
|
||||||
|
def db_type(self, connection=None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def m2m_db_table(self):
|
||||||
|
return self.through._meta.db_table
|
||||||
|
|
||||||
|
def extra_filters(self, pieces, pos, negate):
|
||||||
|
if negate or not self.use_gfk:
|
||||||
|
return []
|
||||||
|
prefix = "__".join(["tagged_items"] + pieces[:pos-2])
|
||||||
|
cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model))
|
||||||
|
if len(cts) == 1:
|
||||||
|
return [("%s__content_type" % prefix, cts[0])]
|
||||||
|
return [("%s__content_type__in" % prefix, cts)]
|
||||||
|
|
||||||
|
def bulk_related_objects(self, new_objs, using):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class _TaggableManager(models.Manager):
|
||||||
|
def __init__(self, through, model, instance):
|
||||||
|
self.through = through
|
||||||
|
self.model = model
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
def get_query_set(self):
|
||||||
|
return self.through.tags_for(self.model, self.instance)
|
||||||
|
|
||||||
|
def _lookup_kwargs(self):
|
||||||
|
return self.through.lookup_kwargs(self.instance)
|
||||||
|
|
||||||
|
@require_instance_manager
|
||||||
|
def add(self, *tags):
|
||||||
|
str_tags = set([
|
||||||
|
t
|
||||||
|
for t in tags
|
||||||
|
if not isinstance(t, self.through.tag_model())
|
||||||
|
])
|
||||||
|
tag_objs = set(tags) - str_tags
|
||||||
|
# If str_tags has 0 elements Django actually optimizes that to not do a
|
||||||
|
# query. Malcolm is very smart.
|
||||||
|
existing = self.through.tag_model().objects.filter(
|
||||||
|
name__in=str_tags
|
||||||
|
)
|
||||||
|
tag_objs.update(existing)
|
||||||
|
|
||||||
|
for new_tag in str_tags - set(t.name for t in existing):
|
||||||
|
tag_objs.add(self.through.tag_model().objects.create(name=new_tag))
|
||||||
|
|
||||||
|
for tag in tag_objs:
|
||||||
|
self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs())
|
||||||
|
|
||||||
|
@require_instance_manager
|
||||||
|
def set(self, *tags):
|
||||||
|
self.clear()
|
||||||
|
self.add(*tags)
|
||||||
|
|
||||||
|
@require_instance_manager
|
||||||
|
def remove(self, *tags):
|
||||||
|
self.through.objects.filter(**self._lookup_kwargs()).filter(
|
||||||
|
tag__name__in=tags).delete()
|
||||||
|
|
||||||
|
@require_instance_manager
|
||||||
|
def clear(self):
|
||||||
|
self.through.objects.filter(**self._lookup_kwargs()).delete()
|
||||||
|
|
||||||
|
def most_common(self):
|
||||||
|
return self.get_query_set().annotate(
|
||||||
|
num_times=models.Count(self.through.tag_relname())
|
||||||
|
).order_by('-num_times')
|
||||||
|
|
||||||
|
@require_instance_manager
|
||||||
|
def similar_objects(self):
|
||||||
|
lookup_kwargs = self._lookup_kwargs()
|
||||||
|
lookup_keys = sorted(lookup_kwargs)
|
||||||
|
qs = self.through.objects.values(*lookup_kwargs.keys())
|
||||||
|
qs = qs.annotate(n=models.Count('pk'))
|
||||||
|
qs = qs.exclude(**lookup_kwargs)
|
||||||
|
qs = qs.filter(tag__in=self.all())
|
||||||
|
qs = qs.order_by('-n')
|
||||||
|
|
||||||
|
# TODO: This all feels like a bit of a hack.
|
||||||
|
items = {}
|
||||||
|
if len(lookup_keys) == 1:
|
||||||
|
# Can we do this without a second query by using a select_related()
|
||||||
|
# somehow?
|
||||||
|
f = self.through._meta.get_field_by_name(lookup_keys[0])[0]
|
||||||
|
objs = f.rel.to._default_manager.filter(**{
|
||||||
|
"%s__in" % f.rel.field_name: [r["content_object"] for r in qs]
|
||||||
|
})
|
||||||
|
for obj in objs:
|
||||||
|
items[(getattr(obj, f.rel.field_name),)] = obj
|
||||||
|
else:
|
||||||
|
preload = {}
|
||||||
|
for result in qs:
|
||||||
|
preload.setdefault(result['content_type'], set())
|
||||||
|
preload[result["content_type"]].add(result["object_id"])
|
||||||
|
|
||||||
|
for ct, obj_ids in preload.iteritems():
|
||||||
|
ct = ContentType.objects.get_for_id(ct)
|
||||||
|
for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids):
|
||||||
|
items[(ct.pk, obj.pk)] = obj
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for result in qs:
|
||||||
|
obj = items[
|
||||||
|
tuple(result[k] for k in lookup_keys)
|
||||||
|
]
|
||||||
|
obj.similar_tags = result["n"]
|
||||||
|
results.append(obj)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _get_subclasses(model):
|
||||||
|
subclasses = [model]
|
||||||
|
for f in model._meta.get_all_field_names():
|
||||||
|
field = model._meta.get_field_by_name(f)[0]
|
||||||
|
if (isinstance(field, RelatedObject) and
|
||||||
|
getattr(field.field.rel, "parent_link", None)):
|
||||||
|
subclasses.extend(_get_subclasses(field.model))
|
||||||
|
return subclasses
|
|
@ -0,0 +1,250 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.contenttypes.generic import GenericForeignKey
|
||||||
|
from django.db import connection, models, IntegrityError, transaction
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
from django.template.defaultfilters import slugify as default_slugify
|
||||||
|
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||||
|
qn = connection.ops.quote_name
|
||||||
|
|
||||||
|
class TagBase(models.Model):
|
||||||
|
name = models.CharField(verbose_name=_('Name'), max_length=100)
|
||||||
|
slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk and not self.slug:
|
||||||
|
self.slug = self.slugify(self.name)
|
||||||
|
if django.VERSION >= (1, 2):
|
||||||
|
from django.db import router
|
||||||
|
using = kwargs.get("using") or router.db_for_write(
|
||||||
|
type(self), instance=self)
|
||||||
|
# Make sure we write to the same db for all attempted writes,
|
||||||
|
# with a multi-master setup, theoretically we could try to
|
||||||
|
# write and rollback on different DBs
|
||||||
|
kwargs["using"] = using
|
||||||
|
trans_kwargs = {"using": using}
|
||||||
|
else:
|
||||||
|
trans_kwargs = {}
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
i += 1
|
||||||
|
try:
|
||||||
|
sid = transaction.savepoint(**trans_kwargs)
|
||||||
|
res = super(TagBase, self).save(*args, **kwargs)
|
||||||
|
transaction.savepoint_commit(sid, **trans_kwargs)
|
||||||
|
return res
|
||||||
|
except IntegrityError:
|
||||||
|
transaction.savepoint_rollback(sid, **trans_kwargs)
|
||||||
|
self.slug = self.slugify(self.name, i)
|
||||||
|
else:
|
||||||
|
return super(TagBase, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def slugify(self, tag, i=None):
|
||||||
|
slug = default_slugify(tag)
|
||||||
|
if i is not None:
|
||||||
|
slug += "_%d" % i
|
||||||
|
return slug
|
||||||
|
|
||||||
|
|
||||||
|
class TagManager(models.Manager):
|
||||||
|
def tags_for_queryset(self, queryset, counts=True, min_count=None):
|
||||||
|
"""
|
||||||
|
Obtain a list of tags associated with instances of a model
|
||||||
|
contained in the given queryset.
|
||||||
|
|
||||||
|
If ``counts`` is True, a ``count`` attribute will be added to
|
||||||
|
each tag, indicating how many times it has been used against
|
||||||
|
the Model class in question.
|
||||||
|
|
||||||
|
If ``min_count`` is given, only tags which have a ``count``
|
||||||
|
greater than or equal to ``min_count`` will be returned.
|
||||||
|
Passing a value for ``min_count`` implies ``counts=True``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
compiler = queryset.query.get_compiler(using='default')
|
||||||
|
extra_joins = ' '.join(compiler.get_from_clause()[0][1:])
|
||||||
|
where, params = queryset.query.where.as_sql(
|
||||||
|
compiler.quote_name_unless_alias, compiler.connection
|
||||||
|
)
|
||||||
|
|
||||||
|
if where:
|
||||||
|
extra_criteria = 'AND %s' % where
|
||||||
|
else:
|
||||||
|
extra_criteria = ''
|
||||||
|
return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
|
||||||
|
"""
|
||||||
|
Perform the custom SQL query for ``usage_for_model`` and
|
||||||
|
``usage_for_queryset``.
|
||||||
|
"""
|
||||||
|
if min_count is not None: counts = True
|
||||||
|
|
||||||
|
model_table = qn(model._meta.db_table)
|
||||||
|
model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
|
||||||
|
FROM
|
||||||
|
%(tag)s
|
||||||
|
INNER JOIN %(tagged_item_alias)s
|
||||||
|
ON %(tag)s.id = %(tagged_item)s.tag_id
|
||||||
|
INNER JOIN %(model)s
|
||||||
|
ON %(tagged_item)s.object_id = %(model_pk)s
|
||||||
|
%%s
|
||||||
|
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||||
|
%%s
|
||||||
|
GROUP BY %(tag)s.id, %(tag)s.name
|
||||||
|
%%s
|
||||||
|
ORDER BY %(tag)s.name ASC""" % {
|
||||||
|
'tag': qn(Tag._meta.db_table),
|
||||||
|
'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
|
||||||
|
'tagged_item_alias': qn(TaggedItem._meta.db_table) + " tagged_item_alias",
|
||||||
|
'tagged_item': "tagged_item_alias",
|
||||||
|
'model': model_table,
|
||||||
|
'model_pk': model_pk,
|
||||||
|
'content_type_id': ContentType.objects.get_for_model(model).pk,
|
||||||
|
}
|
||||||
|
|
||||||
|
min_count_sql = ''
|
||||||
|
if min_count is not None:
|
||||||
|
min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
|
||||||
|
params.append(min_count)
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
|
||||||
|
tags = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
t = Tag(*row[:2])
|
||||||
|
if counts:
|
||||||
|
t.count = row[2]
|
||||||
|
tags.append(t)
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(TagBase):
|
||||||
|
objects = TagManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Tag")
|
||||||
|
verbose_name_plural = _("Tags")
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
self_dict = {
|
||||||
|
'id': self.pk,
|
||||||
|
'name': self.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self_dict
|
||||||
|
|
||||||
|
|
||||||
|
class ItemBase(models.Model):
|
||||||
|
def __unicode__(self):
|
||||||
|
return ugettext("%(object)s tagged with %(tag)s") % {
|
||||||
|
"object": self.content_object,
|
||||||
|
"tag": self.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tag_model(cls):
|
||||||
|
return cls._meta.get_field_by_name("tag")[0].rel.to
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tag_relname(cls):
|
||||||
|
return cls._meta.get_field_by_name('tag')[0].rel.related_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lookup_kwargs(cls, instance):
|
||||||
|
return {
|
||||||
|
'content_object': instance
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bulk_lookup_kwargs(cls, instances):
|
||||||
|
return {
|
||||||
|
"content_object__in": instances,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItemBase(ItemBase):
|
||||||
|
tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tags_for(cls, model, instance=None):
|
||||||
|
if instance is not None:
|
||||||
|
return cls.tag_model().objects.filter(**{
|
||||||
|
'%s__content_object' % cls.tag_relname(): instance
|
||||||
|
})
|
||||||
|
return cls.tag_model().objects.filter(**{
|
||||||
|
'%s__content_object__isnull' % cls.tag_relname(): False
|
||||||
|
}).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class GenericTaggedItemBase(ItemBase):
|
||||||
|
object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True)
|
||||||
|
if django.VERSION < (1, 2):
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
ContentType,
|
||||||
|
verbose_name=_('Content type'),
|
||||||
|
related_name="%(class)s_tagged_items"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
ContentType,
|
||||||
|
verbose_name=_('Content type'),
|
||||||
|
related_name="%(app_label)s_%(class)s_tagged_items"
|
||||||
|
)
|
||||||
|
content_object = GenericForeignKey()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract=True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lookup_kwargs(cls, instance):
|
||||||
|
return {
|
||||||
|
'object_id': instance.pk,
|
||||||
|
'content_type': ContentType.objects.get_for_model(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bulk_lookup_kwargs(cls, instances):
|
||||||
|
# TODO: instances[0], can we assume there are instances.
|
||||||
|
return {
|
||||||
|
"object_id__in": [instance.pk for instance in instances],
|
||||||
|
"content_type": ContentType.objects.get_for_model(instances[0]),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tags_for(cls, model, instance=None):
|
||||||
|
ct = ContentType.objects.get_for_model(model)
|
||||||
|
kwargs = {
|
||||||
|
"%s__content_type" % cls.tag_relname(): ct
|
||||||
|
}
|
||||||
|
if instance is not None:
|
||||||
|
kwargs["%s__object_id" % cls.tag_relname()] = instance.pk
|
||||||
|
return cls.tag_model().objects.filter(**kwargs).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItem(GenericTaggedItemBase, TaggedItemBase):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Tagged Item")
|
||||||
|
verbose_name_plural = _("Tagged Items")
|
|
@ -0,0 +1,141 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.utils.encoding import force_unicode
|
||||||
|
from django.utils.functional import wraps
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from .models import TaggedItem
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags_for_queryset(queryset, tags_attribute='tags'):
|
||||||
|
"""
|
||||||
|
Given a queryset and a taggint atributte returns a list with the form
|
||||||
|
[{'count': number_of_tagged_items, 'tags_attribute': 'id_of the tag'}]
|
||||||
|
[{'count': 3, 'tags': 1}, {'count': 2, 'tags': 2}, {'count': 1, 'tags': 3}]
|
||||||
|
"""
|
||||||
|
return queryset.values(tags_attribute).annotate(count=Count(tags_attribute)).order_by('-count')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tags(tagstring):
|
||||||
|
"""
|
||||||
|
Parses tag input, with multiple word input being activated and
|
||||||
|
delineated by commas and double quotes. Quotes take precedence, so
|
||||||
|
they may contain commas.
|
||||||
|
|
||||||
|
Returns a sorted list of unique tag names.
|
||||||
|
|
||||||
|
Ported from Jonathan Buchanan's `django-tagging
|
||||||
|
<http://django-tagging.googlecode.com/>`_
|
||||||
|
"""
|
||||||
|
if not tagstring:
|
||||||
|
return []
|
||||||
|
|
||||||
|
tagstring = force_unicode(tagstring)
|
||||||
|
|
||||||
|
# Special case - if there are no commas or double quotes in the
|
||||||
|
# input, we don't *do* a recall... I mean, we know we only need to
|
||||||
|
# split on spaces.
|
||||||
|
if u',' not in tagstring and u'"' not in tagstring:
|
||||||
|
words = list(set(split_strip(tagstring, u' ')))
|
||||||
|
words.sort()
|
||||||
|
return words
|
||||||
|
|
||||||
|
words = []
|
||||||
|
buffer = []
|
||||||
|
# Defer splitting of non-quoted sections until we know if there are
|
||||||
|
# any unquoted commas.
|
||||||
|
to_be_split = []
|
||||||
|
saw_loose_comma = False
|
||||||
|
open_quote = False
|
||||||
|
i = iter(tagstring)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
c = i.next()
|
||||||
|
if c == u'"':
|
||||||
|
if buffer:
|
||||||
|
to_be_split.append(u''.join(buffer))
|
||||||
|
buffer = []
|
||||||
|
# Find the matching quote
|
||||||
|
open_quote = True
|
||||||
|
c = i.next()
|
||||||
|
while c != u'"':
|
||||||
|
buffer.append(c)
|
||||||
|
c = i.next()
|
||||||
|
if buffer:
|
||||||
|
word = u''.join(buffer).strip()
|
||||||
|
if word:
|
||||||
|
words.append(word)
|
||||||
|
buffer = []
|
||||||
|
open_quote = False
|
||||||
|
else:
|
||||||
|
if not saw_loose_comma and c == u',':
|
||||||
|
saw_loose_comma = True
|
||||||
|
buffer.append(c)
|
||||||
|
except StopIteration:
|
||||||
|
# If we were parsing an open quote which was never closed treat
|
||||||
|
# the buffer as unquoted.
|
||||||
|
if buffer:
|
||||||
|
if open_quote and u',' in buffer:
|
||||||
|
saw_loose_comma = True
|
||||||
|
to_be_split.append(u''.join(buffer))
|
||||||
|
if to_be_split:
|
||||||
|
if saw_loose_comma:
|
||||||
|
delimiter = u','
|
||||||
|
else:
|
||||||
|
delimiter = u' '
|
||||||
|
for chunk in to_be_split:
|
||||||
|
words.extend(split_strip(chunk, delimiter))
|
||||||
|
words = list(set(words))
|
||||||
|
words.sort()
|
||||||
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
def split_strip(string, delimiter=u','):
|
||||||
|
"""
|
||||||
|
Splits ``string`` on ``delimiter``, stripping each resulting string
|
||||||
|
and returning a list of non-empty strings.
|
||||||
|
|
||||||
|
Ported from Jonathan Buchanan's `django-tagging
|
||||||
|
<http://django-tagging.googlecode.com/>`_
|
||||||
|
"""
|
||||||
|
if not string:
|
||||||
|
return []
|
||||||
|
|
||||||
|
words = [w.strip() for w in string.split(delimiter)]
|
||||||
|
return [w for w in words if w]
|
||||||
|
|
||||||
|
|
||||||
|
def edit_string_for_tags(tags):
|
||||||
|
"""
|
||||||
|
Given list of ``Tag`` instances, creates a string representation of
|
||||||
|
the list suitable for editing by the user, such that submitting the
|
||||||
|
given string representation back without changing it will give the
|
||||||
|
same list of tags.
|
||||||
|
|
||||||
|
Tag names which contain commas will be double quoted.
|
||||||
|
|
||||||
|
If any tag name which isn't being quoted contains whitespace, the
|
||||||
|
resulting string of tag names will be comma-delimited, otherwise
|
||||||
|
it will be space-delimited.
|
||||||
|
|
||||||
|
Ported from Jonathan Buchanan's `django-tagging
|
||||||
|
<http://django-tagging.googlecode.com/>`_
|
||||||
|
"""
|
||||||
|
names = []
|
||||||
|
for tag in tags:
|
||||||
|
name = tag.name
|
||||||
|
if u',' in name or u' ' in name:
|
||||||
|
names.append('"%s"' % name)
|
||||||
|
else:
|
||||||
|
names.append(name)
|
||||||
|
return u', '.join(sorted(names))
|
||||||
|
|
||||||
|
|
||||||
|
def require_instance_manager(func):
|
||||||
|
@wraps(func)
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
if self.instance is None:
|
||||||
|
raise TypeError("Can't call %s with a non-instance manager" % func.__name__)
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return inner
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.conf.urls import patterns, include, url
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# Examples:
|
||||||
|
# url(r'^$', 'greenmine.views.home', name='home'),
|
||||||
|
# url(r'^greenmine/', include('greenmine.foo.urls')),
|
||||||
|
|
||||||
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
)
|
|
@ -0,0 +1,62 @@
|
||||||
|
from django.db import models
|
||||||
|
from .fields import WikiField
|
||||||
|
|
||||||
|
class WikiPage(models.Model):
|
||||||
|
project = models.ForeignKey('scrum.Project', related_name='wiki_pages')
|
||||||
|
slug = models.SlugField(max_length=500, db_index=True)
|
||||||
|
content = WikiField(blank=False, null=True)
|
||||||
|
owner = models.ForeignKey("auth.User", related_name="wiki_pages", null=True)
|
||||||
|
|
||||||
|
watchers = models.ManyToManyField('auth.User',
|
||||||
|
related_name='wikipage_watchers', null=True)
|
||||||
|
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return ('wiki-page', (),
|
||||||
|
{'pslug': self.project.slug, 'wslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_view_url(self):
|
||||||
|
return ('wiki-page', (),
|
||||||
|
{'pslug': self.project.slug, 'wslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_edit_url(self):
|
||||||
|
return ('wiki-page-edit', (),
|
||||||
|
{'pslug': self.project.slug, 'wslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_delete_url(self):
|
||||||
|
return ('wiki-page-delete', (),
|
||||||
|
{'pslug': self.project.slug, 'wslug': self.slug})
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_history_view_url(self):
|
||||||
|
return ('wiki-page-history', (),
|
||||||
|
{'pslug': self.project.slug, 'wslug': self.slug})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WikiPageHistory(models.Model):
|
||||||
|
wikipage = models.ForeignKey("WikiPage", related_name="history_entries")
|
||||||
|
content = WikiField(blank=True, null=True)
|
||||||
|
created_date = models.DateTimeField()
|
||||||
|
owner = models.ForeignKey("auth.User", related_name="wiki_page_historys")
|
||||||
|
|
||||||
|
# TODO: fix this permalink. this implementation is bad for performance.
|
||||||
|
|
||||||
|
@models.permalink
|
||||||
|
def get_history_view_url(self):
|
||||||
|
return ('wiki-page-history-view', (),
|
||||||
|
{'pslug': self.wikipage.project.slug, 'wslug': self.wikipage.slug, 'hpk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class WikiPageAttachment(models.Model):
|
||||||
|
wikipage = models.ForeignKey('WikiPage', related_name='attachments')
|
||||||
|
owner = models.ForeignKey("auth.User", related_name="wikifiles")
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
attached_file = models.FileField(upload_to="files/wiki",
|
||||||
|
max_length=500, null=True, blank=True)
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
from haystack import indexes
|
||||||
|
from .models import WikiPage
|
||||||
|
|
||||||
|
|
||||||
|
class WikiPageIndex(indexes.RealTimeSearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/wikipage_text.txt')
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return WikiPage
|
||||||
|
|
||||||
|
def index_queryset(self):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{ object.project }}
|
||||||
|
{{ object.slug }}
|
||||||
|
{{ object.content }}
|
||||||
|
{{ object.owner }}
|
||||||
|
{{ object.created_date }}
|
||||||
|
{% for watcher in object.watchers.all %}
|
||||||
|
{{ watcher }}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""
|
||||||
|
WSGI config for greenmine project.
|
||||||
|
|
||||||
|
This module contains the WSGI application used by Django's development server
|
||||||
|
and any production WSGI deployments. It should expose a module-level variable
|
||||||
|
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||||
|
this application via the ``WSGI_APPLICATION`` setting.
|
||||||
|
|
||||||
|
Usually you will have the standard Django WSGI application here, but it also
|
||||||
|
might make sense to replace the whole Django WSGI application with a custom one
|
||||||
|
that later delegates to the Django one. For example, you could introduce WSGI
|
||||||
|
middleware here, or combine a Django application with an application of another
|
||||||
|
framework.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
|
||||||
|
# if running multiple sites in the same mod_wsgi process. To fix this, use
|
||||||
|
# mod_wsgi daemon mode with each site in its own daemon process, or use
|
||||||
|
# os.environ["DJANGO_SETTINGS_MODULE"] = "greenmine.settings"
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "greenmine.settings")
|
||||||
|
|
||||||
|
# This application object is used by any WSGI server configured to use this
|
||||||
|
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||||
|
# setting points here.
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
application = get_wsgi_application()
|
||||||
|
|
||||||
|
# Apply WSGI middleware here.
|
||||||
|
# from helloworld.wsgi import HelloWorldApplication
|
||||||
|
# application = HelloWorldApplication(application)
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "greenmine.settings")
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,3 @@
|
||||||
|
django
|
||||||
|
django-grappelli
|
||||||
|
django-tastypie
|
Loading…
Reference in New Issue