Initial version

remotes/origin/enhancement/email-actions
Jesús Espino 2013-03-19 18:53:07 +01:00
parent 88fb1fedbc
commit 5964845710
67 changed files with 4403 additions and 0 deletions

8
.gitignore vendored
View File

@ -0,0 +1,8 @@
.*.sw*
*.log
src/greenmine/settings/local.py
src/database.sqlite
src/logs
src/media
*.pyc
*.mo

1
doc/.gitignore vendored Executable file
View File

@ -0,0 +1 @@
build

153
doc/Makefile Executable file
View File

@ -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."

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
[theme]
inherit = basic
stylesheet = celery.css
[options]

252
doc/source/conf.py Executable file
View File

@ -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'

42
doc/source/index.rst Executable file
View File

@ -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`

77
doc/source/overview.rst Executable file
View File

@ -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

22
doc/source/settings.rst Executable file
View File

@ -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
greenmine/__init__.py Normal file
View File

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

39
greenmine/base/models.py Normal file
View File

@ -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()

126
greenmine/base/tests.py Normal file
View File

@ -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)

View File

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,8 @@
{{ object.title }}
{{ object.slug }}
{{ object.description }}
{{ object.created_date }}
{{ object.modified_date }}
{{ object.project }}
{{ object.owner }}
{{ object.attached_file }}

View File

@ -0,0 +1,2 @@
i# -*- coding: utf-8 -*-
from .documents import *

View File

@ -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 *

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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
}
}
]

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -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')

View File

@ -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()

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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")),
)

View File

@ -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"
}
}
]

View File

View File

@ -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)

809
greenmine/scrum/models.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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]])

View File

@ -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 %}

View File

@ -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 %}

64
greenmine/scrum/utils.py Normal file
View File

@ -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()

View File

@ -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 *

View File

@ -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'),
]
},
}

View File

@ -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 *

View File

@ -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",
]

View File

@ -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

View File

@ -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']

View File

@ -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')

View File

@ -0,0 +1 @@
VERSION = (0, 9, 3)

19
greenmine/taggit/admin.py Normal file
View File

@ -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)

View File

@ -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."

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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."

View File

@ -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)\"."

View File

@ -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

250
greenmine/taggit/models.py Normal file
View File

@ -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")

141
greenmine/taggit/utils.py Normal file
View File

@ -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

12
greenmine/urls.py Normal file
View File

@ -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)),
)

View File

62
greenmine/wiki/models.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1,8 @@
{{ object.project }}
{{ object.slug }}
{{ object.content }}
{{ object.owner }}
{{ object.created_date }}
{% for watcher in object.watchers.all %}
{{ watcher }}
{% endfor %}

32
greenmine/wsgi.py Normal file
View File

@ -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)

10
manage.py Normal file
View File

@ -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)

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
django
django-grappelli
django-tastypie