Merge branch 'release/2.6.2.0'
10
__init__.py
|
@ -8,9 +8,11 @@
|
|||
'''
|
||||
from trytond.pool import Pool
|
||||
|
||||
from project import WebSite, ProjectUsers, ProjectInvitation, \
|
||||
ProjectWorkInvitation, Project, Tag, TaskTags, \
|
||||
ProjectHistory, ProjectWorkCommit
|
||||
from project import (
|
||||
WebSite, ProjectUsers, ProjectInvitation,
|
||||
ProjectWorkInvitation, TimesheetEmployeeDay, Project, Tag,
|
||||
TaskTags, ProjectHistory, ProjectWorkCommit, Activity,
|
||||
)
|
||||
from company import Company, CompanyProjectAdmins, NereidUser
|
||||
|
||||
|
||||
|
@ -22,11 +24,13 @@ def register():
|
|||
ProjectUsers,
|
||||
ProjectInvitation,
|
||||
ProjectWorkInvitation,
|
||||
TimesheetEmployeeDay,
|
||||
Project,
|
||||
Tag,
|
||||
TaskTags,
|
||||
ProjectHistory,
|
||||
ProjectWorkCommit,
|
||||
Activity,
|
||||
Company,
|
||||
CompanyProjectAdmins,
|
||||
NereidUser,
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# 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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@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 " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@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/NereidProject.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NereidProject.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/NereidProject"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NereidProject"
|
||||
@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."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@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."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
|
@ -0,0 +1,252 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# NereidProject documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri May 10 12:36:01 2013.
|
||||
#
|
||||
# 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
|
||||
import ConfigParser
|
||||
|
||||
# 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 = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.ifconfig']
|
||||
|
||||
# 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'NereidProject'
|
||||
copyright = u'2013, Openlabs Technologies & Consulting (P) Limited'
|
||||
|
||||
# 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.
|
||||
config = ConfigParser.ConfigParser()
|
||||
config.readfp(open('../../tryton.cfg'))
|
||||
info = dict(config.items('tryton'))
|
||||
version = info.get('version')
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = info.get('version')
|
||||
|
||||
# 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 = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- 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 = 'nature'
|
||||
|
||||
# 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 = []
|
||||
|
||||
# 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 = {}
|
||||
|
||||
# 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 = 'NereidProjectdoc'
|
||||
|
||||
|
||||
# -- 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', 'NereidProject.tex', u'NereidProject Documentation',
|
||||
u'Openlabs Technologies \\& Consulting (P) Limited', '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', 'nereidproject', u'NereidProject Documentation',
|
||||
[u'Openlabs Technologies & Consulting (P) Limited'], 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', 'NereidProject', u'NereidProject Documentation',
|
||||
u'Openlabs Technologies & Consulting (P) Limited', 'NereidProject', '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'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 132 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 52 KiB |
|
@ -0,0 +1,25 @@
|
|||
.. NereidProject documentation master file, created by
|
||||
sphinx-quickstart on Fri May 10 12:36:01 2013.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to NereidProject's documentation!
|
||||
=========================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
project
|
||||
install
|
||||
quickstart
|
||||
tutorial
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
.. _installation:
|
||||
|
||||
Installation
|
||||
=============
|
||||
|
||||
Install required OS packages
|
||||
----------------------------
|
||||
|
||||
Some requirements to get installed, they might be in your package manager::
|
||||
|
||||
sudo apt-get install python-dev
|
||||
sudo apt-get install python-pip
|
||||
|
||||
These are required for the Python package lxml::
|
||||
|
||||
sudo apt-get install libxml2-dev
|
||||
sudo apt-get install libxslt-dev
|
||||
|
||||
Virtual Environment
|
||||
--------------------
|
||||
|
||||
For getting this running on your local machine, the easy way to do that is
|
||||
setting up the `virtualenvwrapper`_ first.
|
||||
|
||||
.. _virtualenvwrapper:
|
||||
|
||||
virtualenvwrapper
|
||||
.................
|
||||
|
||||
`virtualenvwrappers`_ are isolated Python environments. This helps isolate your
|
||||
dependencies, especially when used with pip. virtualenvwrapper provides some
|
||||
convenient short-hand shell commands to make virtualenv nicer to use.
|
||||
|
||||
If you are on Mac OS X or Linux, the following command will work for you in
|
||||
creating a virtualenv
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ sudo pip install virtualenvwrapper
|
||||
|
||||
Set up virtualenvwrapper
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In your shell initialisation file
|
||||
(eg. ~/.bashrc), add two lines like this::
|
||||
|
||||
export WORKON_HOME=$HOME/.virtualenvs
|
||||
source /usr/local/bin/virtualenvwrapper.sh
|
||||
|
||||
(Change the path to virtualenvwrapper.sh depending on where it was installed by
|
||||
pip.)
|
||||
|
||||
WORKON_HOME is a directory where virtualenvwrapper is going to collect the
|
||||
virtualenvs that you use it to create.
|
||||
|
||||
virtualenvwrapper provides the following commands::
|
||||
|
||||
mkvirtualenv foo
|
||||
rmvirtualenv foo
|
||||
workon foo # activate the virtualenv called foo
|
||||
deactivate # whatever the currently active virtualenv is
|
||||
|
||||
Now ``Create`` a new virtualenv for project:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ mkvirtualenv myproject
|
||||
New python executable in myproject/bin/python
|
||||
Installing setuptools............done.
|
||||
Installing pip...............done.
|
||||
$ cd myproject
|
||||
$ cdvirtualenv
|
||||
|
||||
|
||||
Now, whenever you want to work on a project, you only have to activate the
|
||||
corresponding environment. On OS X and Linux, do the following
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ . venv/bin/activate
|
||||
|
||||
If you are a Ubuntu user, the following command is for you
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ workon myproject
|
||||
(myproject)$
|
||||
|
||||
Either way, you should now be using your virtualenv (notice how the prompt of
|
||||
your shell has changed to show the active environment).
|
||||
|
||||
How to install Nereid Project
|
||||
-----------------------------
|
||||
|
||||
Nereid Project(a Project management system), has been implemented as a Web
|
||||
application to be accessed using a web browser.
|
||||
|
||||
To experience this you should have installed ``tryton client``. Following
|
||||
commands is to be followed for installing Nereid Project's desktop client.
|
||||
|
||||
Nereid project can be installed like any other tryton module or python package
|
||||
as it comes bundled with a setup.py script.
|
||||
Alternatively the latest released version published to PYPI can be installed
|
||||
using PIP.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ pip install trytond_nereid_project
|
||||
|
||||
A few seconds later and you are good to go.
|
||||
|
||||
So to start with, install following::
|
||||
|
||||
pip install psycopg2
|
||||
pip install blinker
|
||||
|
||||
Both above packages should be installed by default but just in case to make
|
||||
sure, it they were not, they get installed in your current working environment.
|
||||
|
||||
Now, nereid-project is installed, to run the web app, Project Management system,
|
||||
refer :ref:`quickstart`.
|
||||
|
||||
.. _virtualenvwrappers: http://virtualenvwrapper.readthedocs.org/en/latest/
|
|
@ -0,0 +1,62 @@
|
|||
.. _nereid_project:
|
||||
|
||||
Welcome To Nereid Project
|
||||
=========================
|
||||
|
||||
This documentation is divided into different parts. We recommend that you get
|
||||
started with :ref:`installation` and then head over to the :ref:`quickstart`.
|
||||
|
||||
**Nereid Project** is an open-source collaborative development platform offered
|
||||
by Team Openlabs. It is mainly used for managing project processes. While it
|
||||
could be used for managing any kind of project, it is primarily used at
|
||||
Openlabs to manage software projects. It is designed to help organise projects
|
||||
& tasks. The aim is to connect everything together on a single interface,
|
||||
avoiding unnecessary time consumption, and track project's progress, task's
|
||||
status, shared files, time spent on individual tasks.
|
||||
|
||||
* Break project into multiple tasks, assign to project teammates to collaborate
|
||||
* Gantt chart provides deep insights about progress
|
||||
* Collaborative dashboard ties everything together
|
||||
* Upload files from personal desktop, or internet link
|
||||
* Organize efforts-Easily create, assign, and comment on tasks, so user always
|
||||
know what's getting done and who's doing it.
|
||||
* Puts tasks together, so user can go to one place for all the history of the
|
||||
work.
|
||||
* notifications via email make it effortless to stay on top of the details that
|
||||
matter to user.
|
||||
|
||||
and much more...
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The goal of nereid project is to provide a friendly web based user interface to
|
||||
stakeholders outside the company to the powerful project management module of
|
||||
Tryton.
|
||||
|
||||
* Separate user accounts for users outside the company (like customers) without
|
||||
giving access to Tryton.
|
||||
|
||||
* Simplify the project management tasks to encourage participation from users
|
||||
who may not be tech savvy.
|
||||
|
||||
Nereid User
|
||||
-----------
|
||||
|
||||
Nereid introduces a model of user management different from the default user
|
||||
management schema (res.user) of Tryton. Nereid project also makes use of the
|
||||
concept to provide logins to participants of the project.
|
||||
Internal employees of the company should in addition have their user accounts
|
||||
linked to their employee records so that timesheet entries can be marked by
|
||||
users who are also employees.
|
||||
|
||||
In addition, nereid project also introduces the idea of project administrators.
|
||||
Project administrators are created by adding nereid users to the project admins
|
||||
section on the company module. This is due for deprecation and will be replaced
|
||||
with the permissions system introduced in nereid. Nereid Users with the
|
||||
nereid_project.admin permission will be automatically given admin rights to
|
||||
all projects.
|
||||
|
||||
.. note::
|
||||
Note that the permissions mentioned here are nereid.permissions and not the
|
||||
regular Tryton access control user groups.
|
|
@ -0,0 +1,209 @@
|
|||
.. _quickstart:
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Eager to get started? This page gives a good introduction to Nereid Project.
|
||||
It assumes you already have Nereid and Nereid Project installed. If you do not,
|
||||
head over to then :ref:`installation` section.
|
||||
|
||||
A minimal application
|
||||
---------------------
|
||||
|
||||
A minimal Nereid application first requries a Tryton database with the
|
||||
Nereid module installed. If you already have a database with Nereid
|
||||
installed head over to `creating website`_.
|
||||
|
||||
Setting up a database
|
||||
`````````````````````
|
||||
|
||||
To create a new Tryton database, you will need to fill out the information as on
|
||||
the screenshot:
|
||||
|
||||
* Server connection : localhost:8000
|
||||
* Tryton Server Password: admin
|
||||
* Database name : database_name
|
||||
* Admin password : admin
|
||||
|
||||
.. image:: images/tryton-database.png
|
||||
:align: center
|
||||
|
||||
This is how you can create a new database. Now to login your ERP using the
|
||||
Tryton client, you need the following information:
|
||||
|
||||
* Host : localhost:8000
|
||||
* Database : database_name
|
||||
* User name: 'admin' or Ask your administrator for this information.
|
||||
* Password : 'admin' or Ask your administrator for this information.
|
||||
|
||||
.. _creating website:
|
||||
|
||||
Creating a new website
|
||||
``````````````````````
|
||||
|
||||
Once the nereid module is installed in a Tryton database, open the `Websites`
|
||||
menu under `Nereid/Configuration`, and create a new website with the following
|
||||
settings.
|
||||
|
||||
+-----------+-------------------------------+
|
||||
| **Field** | **Value** |
|
||||
+-----------+-------------------------------+
|
||||
| Name | abcpartnerportal.com |
|
||||
+-----------+-------------------------------+
|
||||
| URL Map | Choose `Default` |
|
||||
+-----------+-------------------------------+
|
||||
| Company | Choose your Company |
|
||||
+-----------+-------------------------------+
|
||||
| Default | English |
|
||||
| Language | |
|
||||
+-----------+-------------------------------+
|
||||
| Guest User| Create a new Nereid User |
|
||||
+-----------+-------------------------------+
|
||||
| App User | Create or choose a User |
|
||||
+-----------+-------------------------------+
|
||||
|
||||
The default language is the language your website is displayed in. When a user
|
||||
visits the root of the website (say example.com), the user will be redirected to
|
||||
abcpartnerportal.com/default_language/
|
||||
|
||||
Example: If English US is selected as Default Language, then the user will be
|
||||
redirected to ``abcpartnerportal.com/en_US/``
|
||||
|
||||
This is the tryton user with which the application will run. Ensure that the
|
||||
user you choose has the sufficient permissions (through groups) to access and
|
||||
update tryton models related to project management.
|
||||
|
||||
.. image:: images/tryton-web-site.png
|
||||
:width: 700
|
||||
|
||||
When the web site is created. It is recommended to create a new Nereid User.
|
||||
Here in above screenshot a new nereid user as a guest is created. But for
|
||||
giving privileges to the project, you need to create other nereid users also,
|
||||
they can be employee of your company, the clients, the stakeholders, etc.
|
||||
|
||||
.. _admin:
|
||||
|
||||
Admin Users
|
||||
------------
|
||||
|
||||
After creating website as mentioned in `creating website`_, create a new
|
||||
admin user as shown below:
|
||||
|
||||
.. image:: images/nereid-admin-user.png
|
||||
:width: 700
|
||||
|
||||
Now we need to set up the created nereid user as project admin, because only
|
||||
project admins are able to create, maintain, the project. And there should
|
||||
be one project admin at least, it entirely depends upon you, how many project
|
||||
admins does anybody wants for a project. For that goto company's module, and
|
||||
for that company add the nereid user in ``Project Admins`` tab.
|
||||
|
||||
.. image:: images/add-admin.png
|
||||
:width: 700
|
||||
|
||||
Refer to the :py:class:`trytond_nereid.routing.WebSite` for details on what
|
||||
each of the fields mean.
|
||||
|
||||
.. tip::
|
||||
Since version 2.0.0.3 the name of the website is used by the WSGI
|
||||
dispatcher to identify the website that needs to be served. When you
|
||||
test the site locally, it is not usually possible to mimic your
|
||||
production url. This can be overcome by using a simple WSGI middleware
|
||||
which overwrite HTTP_HOST in the environ.
|
||||
|
||||
.. _launching_application:
|
||||
|
||||
Launching the application and template
|
||||
```````````````````````````````````````
|
||||
|
||||
Once the website is created, a python script which loads nereid and runs
|
||||
the application needs to be written. This script is used to load Nereid,
|
||||
configure your application settings and also serves as an APP_MODULE if
|
||||
you plan to use WSGI HTTP servers like `Gunicorn`_
|
||||
|
||||
.. note::
|
||||
DATABASE_NAME has to be changed in ``application.py``. Mention the
|
||||
database name you have created while setting the database in tryton client,
|
||||
and also the site name which you mentioned while creating the website.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#!/usr/bin/env python
|
||||
from nereid import Nereid
|
||||
|
||||
CONFIG = dict(
|
||||
|
||||
# The name of database
|
||||
DATABASE_NAME = 'database_name',
|
||||
|
||||
# Static file root. The root location of the static files. The static/ will
|
||||
# point to this location. It is recommended to use the web server to serve
|
||||
# static content
|
||||
STATIC_FILEROOT = 'static/',
|
||||
|
||||
# Tryton Config file path
|
||||
TRYTON_CONFIG = '../etc/trytond.conf',
|
||||
|
||||
# If the application is to be configured in the debug mode
|
||||
DEBUG = False,
|
||||
|
||||
# Load the template from FileSystem in the path below instead of the
|
||||
# default Tryton loader where templates are loaded from Database
|
||||
TEMPLATE_LOADER_CLASS = 'nereid.templating.FileSystemLoader',
|
||||
TEMPLATE_SEARCH_PATH = '.',
|
||||
)
|
||||
|
||||
# Create a new application
|
||||
app = Nereid()
|
||||
|
||||
# Update the configuration with the above config values
|
||||
app.config.update(CONFIG)
|
||||
|
||||
# Initialise the app, connect to cache and backend
|
||||
app.initialise()
|
||||
|
||||
|
||||
class NereidHostChangeMiddleware(object):
|
||||
"""
|
||||
A middleware which alters the HTTP_HOST so that you can test
|
||||
the site locally. This middleware replaces the HTTP_HOST with
|
||||
the value you prove to the :attr: site
|
||||
|
||||
:param app: The application for which the middleware needs to work
|
||||
:param site: The value which should replace HTTP_HOST WSGI Environ
|
||||
"""
|
||||
def __init__(self, app, site):
|
||||
self.app = app
|
||||
self.site = site
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ['HTTP_HOST'] = self.site
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# The name of the website
|
||||
site = 'abcpartnerportal.com'
|
||||
|
||||
app.wsgi_app = NereidHostChangeMiddleware(app.wsgi_app, site)
|
||||
app.debug = True
|
||||
app.static_folder = '%s/static' % site
|
||||
app.run('0.0.0.0')
|
||||
|
||||
You can now test run the application
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ python application.py
|
||||
|
||||
The above command launches a single threaded HTTP Server for debugging
|
||||
purposes which listens to the port 5000. Point your browser to
|
||||
`localhost:5000 <http://localhost:5000/>`_
|
||||
|
||||
.. image:: images/login-page.png
|
||||
:align: center
|
||||
:width: 700
|
||||
|
||||
Now the installation is successful. Refer :ref:`tutorial`.
|
||||
|
||||
.. _Gunicorn: http://gunicorn.org/
|
|
@ -0,0 +1,589 @@
|
|||
.. _tutorial:
|
||||
|
||||
Nereid Project Tutorial
|
||||
=======================
|
||||
|
||||
This tutorial gives an overview into how nereid project is organized and how it
|
||||
works with the Tryton project module. Familiarity with the tryton project module
|
||||
is not assumed, but could make the project easier to understand. Follow
|
||||
:ref:`quickstart` before starting this tutorial.
|
||||
|
||||
Creating Your First Project
|
||||
----------------------------
|
||||
|
||||
Everything within the Project Management system starts out with a project.
|
||||
Projects start out with a simple default process, with two phases/states:
|
||||
opened, and done. Tasks, Tickets, Attachments, all must belong to a project. So
|
||||
let's get started with creating a project...
|
||||
|
||||
When logged in as a project admin, see :ref:`admin`, he can create new projects,
|
||||
invite new or existing users to the project and change settings related to the
|
||||
project. Now when project is created, the whole features regarding that project
|
||||
are visible.
|
||||
|
||||
* Click the New Project button (found at the top right of every Project
|
||||
Management Page)
|
||||
|
||||
* A modal window will then slide into view, where you will find fields for
|
||||
entering the title of the project. Once you are done, click Save.
|
||||
|
||||
* You will now be taken to the Project overview screen, you just created your
|
||||
first project!
|
||||
|
||||
See the screenshot shown below:
|
||||
|
||||
.. _project management screen:
|
||||
|
||||
.. image:: images/project-page.png
|
||||
:width: 900
|
||||
:align: center
|
||||
|
||||
Projects are assigned and related to clients, project managers and the
|
||||
appropriate employees. Each can have separate log-ins that allow them to view
|
||||
their specific projects.
|
||||
|
||||
.. note::
|
||||
Only the project admin <:ref:`admin`> can create the project on Project
|
||||
Management System.
|
||||
|
||||
Creating project on tryton client
|
||||
---------------------------------
|
||||
|
||||
Alternatively projects could be created from your preferred Tryton client as
|
||||
shown below:
|
||||
|
||||
1. Click the Create New button (found at the top left of every form view in
|
||||
tryton)
|
||||
|
||||
2. Where you will find fields for entering the title of the project, the type
|
||||
(whether project or a task), Company, participants or assignee (if any),
|
||||
State of the project(opened or done).
|
||||
|
||||
3. Once you are done, click on Save button found at top left of the form view,
|
||||
next to New button.
|
||||
|
||||
.. image:: images/create-project.png
|
||||
:width: 700
|
||||
:align: center
|
||||
|
||||
Project admin adds the project, or performs any changes through tryton client,
|
||||
it gets updated to web-interface, and vice-versa.
|
||||
|
||||
Adding Participants to Project via tryton client
|
||||
------------------------------------------------
|
||||
|
||||
The project permissions allow project admin to control exactly what he wants
|
||||
his employees to be able to access. The participants to the project can only be
|
||||
added by the project admin through tryton client as shown below in the
|
||||
screenshot, participants are then allowed to do list of following things - can
|
||||
view project, contributes to the project, create tasks, updates the progress
|
||||
made so far, change the state of the task, assign it to other participant of
|
||||
that project, mark their contribution time, etc.
|
||||
|
||||
This below figure shows how to add the project participants on tryton client:
|
||||
|
||||
.. image:: images/project-participant.png
|
||||
:width: 700
|
||||
|
||||
Changing State:
|
||||
```````````````
|
||||
|
||||
To change the status of a project, simply change the drop down value located to
|
||||
the down-left side. It signifies the state of the project whether it is done or
|
||||
opened.
|
||||
|
||||
.. note:: State can be changed only by project admin
|
||||
|
||||
.. _invitation:
|
||||
|
||||
Invite others and manage your team via web interface
|
||||
----------------------------------------------------
|
||||
|
||||
With the multi-user log-ins project admin can control who has access to Project.
|
||||
Company's clients, staff, vendors, will all be kept up to date on the projects
|
||||
they are assigned to.
|
||||
Nereid Project makes it easy to invite others to collaborate on project, and
|
||||
lets project admin organize team members for project. For example, project
|
||||
admin let certain users view specific project but not other project.
|
||||
|
||||
.. image:: images/people-n-permissions.png
|
||||
:width: 900
|
||||
|
||||
The Project Management Screen
|
||||
-----------------------------
|
||||
|
||||
The screenshot shown above, `project management screen`_, is the main page
|
||||
where you'll spend most of you time - the project management screen - the page
|
||||
that let's you view everything relating to your project, everything is on a
|
||||
single page, at your finger tips, right where you need it. It's extremely
|
||||
useful for getting things done and quick.
|
||||
|
||||
So at the very top we have the project title, next we have the following
|
||||
features:
|
||||
|
||||
* **Dashboard:** Where a list of of all projects are shown depending upon the
|
||||
permissions granted to that nereid user. For more information, see
|
||||
`dashboard`_.
|
||||
|
||||
* **Projects Home:** This contains all the project details - project team
|
||||
member it's assigned to, associated client, total time worked and tasks
|
||||
related to projects.
|
||||
|
||||
* **Tasks:** Every single project can have multiple tasks assigned to it.
|
||||
participant of the project can create tasks depending on the requirements to
|
||||
achieve the goal of the project as soon as possible. See `tasks`_.
|
||||
|
||||
* **Time Sheets:** The timesheet module allow to track the time spent by
|
||||
employees on various tasks. This module also comes with several reports that
|
||||
show the time spent by employees. For more refer `timesheet`_.
|
||||
|
||||
* **Planning:** This uses the feature of gantt charts and gantt Charts visually
|
||||
get the big picture or where you are with regards to all of your ongoing
|
||||
tasks. The Nereid Project has a wonderful interface that is completely
|
||||
intuitive.Refer `planning`_.
|
||||
|
||||
* **Files:** Attach and display project related documents in Nereid Project. It
|
||||
is possible to attach files to projects or tasks. Attach and share files in
|
||||
the right context. One can attach many number of files to the task. Later
|
||||
these files can be downloaded/browsed. See more in `files`_.
|
||||
|
||||
* **People and Permissions:** The participants of the project, comes under
|
||||
this, project admin can invite, remove participants from the project from
|
||||
here. See `invitation`_.
|
||||
|
||||
.. _tag:
|
||||
|
||||
* **Tags :** User can also apply tags to tasks within the project management
|
||||
system. As with colors, the meaning of tags is up to user — user might use
|
||||
them to indicate priority, features, category, or any other information to
|
||||
keep them organized into groups or classes of work they find useful. Just
|
||||
select the color, and add title or name along with it.
|
||||
|
||||
* **Estimated Effort** : The estimated effort for a task. See
|
||||
`estimated effort`_.
|
||||
|
||||
..and much more.
|
||||
|
||||
.. _tasks:
|
||||
|
||||
Creating Task
|
||||
--------------
|
||||
|
||||
The ability to define a task, assign it to someone, create a deadline, and know
|
||||
when it's complete — is generally the most desired and ubiquitous feature in
|
||||
project-management system.
|
||||
|
||||
Once a project is created, tasks are assigned to participants who are solely
|
||||
responsible for that task. Nereid Project streamlines the process of adding and
|
||||
assigning tasks.
|
||||
|
||||
While creating task, one can do following things:
|
||||
|
||||
* Due dates on tasks
|
||||
|
||||
* Start dates on tasks
|
||||
|
||||
* Add estimated completion time to each task
|
||||
|
||||
* Assign tasks to participants.
|
||||
|
||||
* Attach files and comments to a task.
|
||||
|
||||
* Notify people about a task.
|
||||
|
||||
Though any participant can create tasks, in the figure shown below, the project
|
||||
admin is creating the task, with the title and description related to that
|
||||
task, can assign it to any of the participants, team members of that project in
|
||||
seconds, can put estimated efforts, start and end date for the task. Project
|
||||
participant can also assign it to other participant of the project.
|
||||
|
||||
.. image:: images/create-task.png
|
||||
:width: 700
|
||||
:align: center
|
||||
|
||||
.. note::
|
||||
Any nereid user having access to project can create task, update the task by
|
||||
putting comments, upload files into it, and assign it to other nereid user
|
||||
of that project. See `update`_.
|
||||
|
||||
.. _reST primer:
|
||||
|
||||
Basic RST primer
|
||||
----------------
|
||||
|
||||
This section is a brief introduction to reStructuredText (reST) concepts and
|
||||
syntax, reST was designed to be a simple, unobtrusive markup language. For more
|
||||
refer `RST primer <http://sphinx-doc.org/rest.html>`_
|
||||
|
||||
Lists
|
||||
`````
|
||||
Just place an asterisk at the start of a paragraph and indent properly. The
|
||||
same goes for numbered lists;they can also be autonumbered using a ``#`` sign::
|
||||
|
||||
* This is a bulleted list.
|
||||
* It has two items, the second
|
||||
item uses two lines.
|
||||
|
||||
1. This is a numbered list.
|
||||
2. It has two items too.
|
||||
|
||||
#. This is a numbered list.
|
||||
#. It has two items too.
|
||||
|
||||
Paragraph
|
||||
`````````
|
||||
As in Python, indentation is significant in reST, so all lines of the same
|
||||
paragraph must be left-aligned to the same level of indentation.
|
||||
|
||||
Inline markup
|
||||
`````````````
|
||||
The standard reST inline markup is quite simple: use
|
||||
|
||||
* one asterisk: ``*text*`` for emphasis (italics),
|
||||
* two asterisks: ``**text**`` for strong emphasis (boldface), and
|
||||
* backquotes: ````text```` for code samples.
|
||||
|
||||
Code Highlighting
|
||||
``````````````````
|
||||
The highlighting language can be changed using the ``highlight`` directive, by
|
||||
default, this is ``'python'`` as the majority of files will have to highlight
|
||||
Python snippets used as follows::
|
||||
|
||||
.. highlight:: c
|
||||
|
||||
An example in python code highlighting::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def some_function():
|
||||
interesting = False
|
||||
print 'This is '
|
||||
print 'code highlighting'
|
||||
print '...'
|
||||
|
||||
.. _update:
|
||||
|
||||
Updating task
|
||||
--------------
|
||||
|
||||
Task updates can be formatted using `reST primer`_ syntax for
|
||||
making comments or updates looks clear. For more `reST(restructured Text)
|
||||
<http://docutils.sourceforge.net/docs/ref/rst/directives.html>`_
|
||||
|
||||
Updates can be written to clarify progress made so far for the task, for
|
||||
changing the state of the task, for marking time i.e., the time spent by the
|
||||
employee on that task etc. While marking time user can also update the `state`_
|
||||
|
||||
|
||||
.. image:: images/task.png
|
||||
:width: 800
|
||||
:align: center
|
||||
|
||||
.. _timesheet:
|
||||
|
||||
Marking Time
|
||||
`````````````
|
||||
|
||||
Nereid Project enables the team to record their time directly on their tasks on
|
||||
every update. Each time the employee comments on a task, the time entered is
|
||||
updated along with it.
|
||||
|
||||
For marking time, see below:
|
||||
|
||||
.. tip::
|
||||
User will need to understand how much time they are devoting to each task
|
||||
and mark time in hours. For marking time in minutes, convert those minutes
|
||||
to hours, like, for entering 6 minutes - mark '.1', for 30 minutes - mark
|
||||
'.5' and so on.
|
||||
|
||||
.. image:: images/time.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
View my-tasks
|
||||
-------------
|
||||
|
||||
Project participants can see their task list, and these lists easily help user
|
||||
to keep track of every assigned tasks on a project, quickly tells the `state`_,
|
||||
and with `tag`_ (if associated to it)!
|
||||
|
||||
.. admonition:: And, by the way...
|
||||
|
||||
Drag and Drop- To change the state of the task, just drag and drop task from
|
||||
one state to the necessary state.
|
||||
|
||||
.. image:: images/my-tasks.png
|
||||
:width: 800
|
||||
:align: center
|
||||
|
||||
View all tasks
|
||||
```````````````
|
||||
|
||||
The employees who get access see all tasks gathered in this project as user
|
||||
might want to see all of the tasks quickly, find a task when don’t remember its
|
||||
name. It can be really helpful to get a comprehensive view of all the tasks.
|
||||
|
||||
* Striped multi-colour tasks in NereidProject- tasks with different colors
|
||||
signifies different `state`_
|
||||
* Make an instant search of a task
|
||||
* All tasks together, so user can go to one place for all the history of the
|
||||
work.
|
||||
|
||||
To see All Tasks, Open Tasks, Done Tasks just click on the ``Tasks``
|
||||
Button shown on the left, for reference see below:
|
||||
|
||||
.. image:: images/tasks-list.png
|
||||
:width: 900
|
||||
:align: center
|
||||
|
||||
.. _state:
|
||||
|
||||
State of Task
|
||||
-------------
|
||||
|
||||
.. image:: images/backlog.png
|
||||
.. image:: images/planning1.png
|
||||
.. image:: images/progress.png
|
||||
.. image:: images/review.png
|
||||
.. image:: images/done.png
|
||||
|
||||
Ideal way the project admin and participants are using to manage their tasks
|
||||
is to specify the state while updating or assigning along with it. This can be
|
||||
considered as the tasks progress. For greater transparency of task these
|
||||
following states are defined:
|
||||
|
||||
* **Backlog:** The task's backlog state is a state containing short
|
||||
descriptions of all functionality desired in the task when assigned to
|
||||
participant. The task backlog state can consists of features, bug fixes,
|
||||
non-functional requirements, etc. - whatever needs to be done in order to
|
||||
deliver it successfully. The default state of task is backlog after being
|
||||
created.
|
||||
* **Planning:** The backlog state can then move into planning, this determines
|
||||
how much of it the user can commit to complete the task.
|
||||
* **In Progress:** Development comes under this state to fulfil the
|
||||
requirements that must end on time.
|
||||
* **Review:** After development state, user can now assign it back to the
|
||||
:ref:`admin`, to review. If requirements are not completed, the state
|
||||
then be back into ``In Progress`` or ``Planning`` or ``Backlog``.
|
||||
* **Done:** If requirements met, the task can then be marked as Done.
|
||||
|
||||
In their simplest, the tasks are categorized into the work stages:
|
||||
|
||||
* from Backlog --> Planning
|
||||
|
||||
* from Planning --> In Progress
|
||||
|
||||
* from In Progress --> Review/ QA
|
||||
|
||||
* from Review/QA --> Done
|
||||
|
||||
Remove participants from task notification
|
||||
```````````````````````````````````````````
|
||||
|
||||
While updating task user can add or remove people among participants to get
|
||||
notified or not, by clicking on ``Notify People`` button. This shows the list
|
||||
of participants of that project. Anytime a task is updated all participants of
|
||||
the project will get notified by e-mail about the progress of the task and a
|
||||
link back to the specific Project task. See below, from where to add-remove
|
||||
participants for the current task:
|
||||
|
||||
.. image:: images/notify.png
|
||||
:width: 800
|
||||
:align: center
|
||||
|
||||
E-mail Notification
|
||||
-------------------
|
||||
|
||||
An integral part to the Nereid Project is email notification. Each project
|
||||
participant as well as the project admin receives an automated email
|
||||
notification whenever there is a change is made to the task. The participants
|
||||
receives notification about new tasks. Project admin receive notification of
|
||||
task completion as well as other task progress state.
|
||||
|
||||
.. estimated effort::
|
||||
|
||||
Estimated Effort
|
||||
`````````````````
|
||||
|
||||
Estimated Effort is a dimension of every task. This allows for time-based
|
||||
completion calculations on every task. As there would be time consumption on
|
||||
each task action. This creates a more routine environment for team members
|
||||
allowing them to spend time on a planned way. So that every task has
|
||||
achievable schedule objectives.
|
||||
The estimated effort required to complete the task could also be filled in when
|
||||
creating the task.
|
||||
|
||||
.. tip::
|
||||
To enter the estimated time afterwards creating the task. Click the
|
||||
``Estimated Hours`` button on the left side of the web-interface, a modal
|
||||
window will slide into view, where you can enter the time.
|
||||
|
||||
.. image:: images/estimated-time.png
|
||||
:align: center
|
||||
:width: 700
|
||||
|
||||
.. _files:
|
||||
|
||||
Dropbox
|
||||
--------
|
||||
|
||||
User can attach files directly to tasks, to help keep everything organized and
|
||||
in one place. Once a file is attached, all participants can access it quickly.
|
||||
The attachment section allows you to upload files to the project or task.
|
||||
|
||||
There are two ways for attaching files to Project Management System:
|
||||
|
||||
* Link(From Internet): Provide the URL from internet source, it stores the link
|
||||
|
||||
* Local(From your Computer): Choose a local file from your system to upload
|
||||
|
||||
.. image:: images/file-upload.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
To upload attachments to Nereid Project, follow these steps:
|
||||
|
||||
* Open up the task to attach a file, click Files button on the left side for
|
||||
attaching files or link, a modal window slide into view and from the
|
||||
drop-down menu, select type to attach i.e., to attach a link from the
|
||||
internet, or file to upload.
|
||||
|
||||
* Select the file/link you'd like to attach. Your file will appear in your task
|
||||
as shown in figure below.
|
||||
|
||||
.. image:: images/upload-file.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
The Files button shows all files that have been attached through individual
|
||||
posted to the task. Files attached to the system are collected and displayed
|
||||
here in Files section, along with filename, the description along with it, and
|
||||
a link to the area where that file is being attached.The original file is
|
||||
included along with a link to download the file.
|
||||
|
||||
.. image:: images/files-button.png
|
||||
:width: 800
|
||||
:align: center
|
||||
|
||||
.. _dashboard:
|
||||
|
||||
Dashboard
|
||||
``````````
|
||||
|
||||
The project dashboard gives a summary of active projects. Nereid Project's
|
||||
Dashboard is a customized project information system containing list of
|
||||
projects, for tracking team progress toward completing an iteration.
|
||||
|
||||
.. tip::
|
||||
Only those projects are visible to user whose permission is provided by
|
||||
project admin.
|
||||
|
||||
.. image:: images/dashboard.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
Global Timesheet
|
||||
-----------------
|
||||
|
||||
For Project Managers, and Owners - this Timesheet information 'completes the
|
||||
picture' of project productivity and progress. Team members do not have access
|
||||
to a global timesheet calendar which details every step within the project
|
||||
timeline. It helps to delegate and track project tasks and manage the projects
|
||||
effectively.
|
||||
This timesheet and online project management application helps to track, or
|
||||
monitor every hour that is spent on a project, by whom and how they did with
|
||||
regards to staying within your expected target durations.
|
||||
|
||||
.. image:: images/global-timesheet.png
|
||||
:align: center
|
||||
:width: 900
|
||||
|
||||
.. tip::
|
||||
Project admin can filter the performance by employees also. See top-left
|
||||
side of this global timesheet page, there is a search box, enter the name of
|
||||
employee to checkout the performance, to track total hours spent by
|
||||
individual on that task. Use timesheet to efficiently record the
|
||||
“Hours Worked” (per Project, or Task). By using this, project admin can view
|
||||
the team's progress and determine whether the team is making sufficient
|
||||
progress.
|
||||
|
||||
.. image:: images/timesheet-lines.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
The timesheet line express the fact that one employee spend a part of his/her
|
||||
time on a specific work at a given date. The list of timesheet lines of
|
||||
employees associated to the project and its tasks. These timesheet lines are
|
||||
used to analyse employee's productivity & job costs.
|
||||
|
||||
Weekly Analysis
|
||||
````````````````
|
||||
|
||||
To gather data weekly on the actual time spent by employee. For time tracking
|
||||
to monitor employees performance. The :ref:`admin` can analyse the progress of
|
||||
the team of the project. Can filter it by employee's name also. Refer image:
|
||||
|
||||
.. image:: images/weekly-analysis.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
Task by employees
|
||||
``````````````````
|
||||
|
||||
A powerful filter for :ref:`admin` to see tasks holders of the projects.
|
||||
Shows list of tasks assigned to particular employee.
|
||||
|
||||
.. image:: images/tasks-employee.png
|
||||
:align: center
|
||||
:width: 800
|
||||
|
||||
.. _planning:
|
||||
|
||||
Calendar
|
||||
`````````
|
||||
|
||||
The calendar is directly tied to the ongoing projects. The calendar show a
|
||||
graphical calendar interface with all of the pertinent ongoing tasks. It is
|
||||
able to filter by month, week or day. Access to calendars and the tasks held
|
||||
within follow the same access, setup for projects. So that users will only see
|
||||
the calendar items of the projects they are invited to. For project admin,
|
||||
calendar provides a number of powerful filters. These filters let project admin
|
||||
see performance of employees. This is a great feature for project admin to
|
||||
track your progress on the graphical Gantt charts for their most highly valued
|
||||
projects.
|
||||
|
||||
.. image:: images/calender.png
|
||||
:width: 900
|
||||
:align: center
|
||||
|
||||
Here the logged in user can view the timesheet of his current project, and also
|
||||
his performance for that project.
|
||||
|
||||
.. note::
|
||||
|
||||
For admin, its easy-to-use, for tracking employee's marked time and
|
||||
performance. The row on timesheet lines shows their name, time they worked
|
||||
for which task. Shows total time, the employee worked per day.
|
||||
|
||||
Project Planning
|
||||
-----------------
|
||||
|
||||
Creating a project plan is the first thing a user should do when taking any kind
|
||||
of project by putting start and end time on its task. Project planning is a
|
||||
feature used to reflect the duration of a task within a certain time period. It
|
||||
is a known fact that a good project plan can make the difference between the
|
||||
success or failure of a project.
|
||||
|
||||
Planning organize, schedule and ensure that tasks get done on time. On short it
|
||||
can boost productivity. By being better organized and more focused on what have
|
||||
to be done, and saves time.
|
||||
|
||||
This feature is used for projects, but only consist of a list of tasks. To
|
||||
access it, go to ``Dashboard ‣ Projects Home ‣ New Project ‣ Planning`` ( Here
|
||||
'New Project' is the name of the selected project ). User can select single
|
||||
project at a time to see the planning. It shows the Gantt chart for tasks with
|
||||
start and end time of task or just the duration.
|
||||
|
||||
|
||||
.. image:: images/planning.png
|
||||
:width: 1000
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
fabfile
|
||||
|
||||
Fab file to build and push documentation to github
|
||||
|
||||
:copyright: © 2013 by Openlabs Technologies & Consulting (P) Limited
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import time
|
||||
|
||||
from fabric.api import local, lcd
|
||||
|
||||
|
||||
def upload_documentation():
|
||||
"""
|
||||
Build and upload the documentation HTML to github
|
||||
"""
|
||||
temp_folder = '/tmp/%s' % time.time()
|
||||
local('mkdir -p %s' % temp_folder)
|
||||
|
||||
# Build the documentation
|
||||
with lcd('doc'):
|
||||
local('make html')
|
||||
local('mv build/html/* %s' % temp_folder)
|
||||
|
||||
# Checkout to gh-pages branch
|
||||
local('git checkout gh-pages')
|
||||
|
||||
# Copy back the files from temp folder
|
||||
local('rm -rf *')
|
||||
local('mv %s/* .' % temp_folder)
|
||||
|
||||
# Add the relevant files
|
||||
local('git add *.html')
|
||||
local('git add *.js')
|
||||
local('git add *.js')
|
||||
local('git add *.inv')
|
||||
local('git add _images')
|
||||
local('git add _sources')
|
||||
local('git add _static')
|
||||
local('git commit -m "Build documentation"')
|
||||
local('git push')
|
398
project.py
|
@ -39,9 +39,9 @@ from trytond.config import CONFIG
|
|||
from trytond.tools import get_smtp_server, datetime_strftime
|
||||
from trytond.backend import TableHandler
|
||||
|
||||
__all__ = ['WebSite', 'ProjectUsers', 'ProjectInvitation', \
|
||||
'ProjectWorkInvitation', 'Project', 'Tag', \
|
||||
'TaskTags', 'ProjectHistory', 'ProjectWorkCommit']
|
||||
__all__ = ['WebSite', 'ProjectUsers', 'ProjectInvitation',
|
||||
'TimesheetEmployeeDay', 'ProjectWorkInvitation', 'Project', 'Tag',
|
||||
'TaskTags', 'ProjectHistory', 'ProjectWorkCommit', 'Activity',]
|
||||
__metaclass__ = PoolMeta
|
||||
|
||||
|
||||
|
@ -209,6 +209,36 @@ class ProjectWorkInvitation(ModelSQL):
|
|||
)
|
||||
|
||||
|
||||
class TimesheetEmployeeDay(ModelView):
|
||||
'Gantt dat view generator'
|
||||
__name__ = 'timesheet_by_employee_by_day'
|
||||
|
||||
employee = fields.Many2One('company.employee', 'Employee')
|
||||
date = fields.Date('Date')
|
||||
hours = fields.Float('Hours', digits=(16, 2))
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
"""
|
||||
Init Method
|
||||
|
||||
:param module_name: Name of the module
|
||||
"""
|
||||
super(TimesheetEmployeeDay, cls).__register__(module_name)
|
||||
|
||||
query = '"timesheet_by_employee_by_day" AS ' \
|
||||
'SELECT timesheet_line.employee, timesheet_line.date, ' \
|
||||
'SUM(timesheet_line.hours) AS sum ' \
|
||||
'FROM "timesheet_line" ' \
|
||||
'GROUP BY timesheet_line.date, timesheet_line.employee;'
|
||||
|
||||
if CONFIG['db_type'] == 'postgres':
|
||||
Transaction().cursor.execute('CREATE OR REPLACE VIEW ' + query)
|
||||
|
||||
elif CONFIG['db_type'] == 'sqlite':
|
||||
Transaction().cursor.execute('CREATE VIEW IF NOT EXISTS ' + query)
|
||||
|
||||
|
||||
class Project:
|
||||
"""
|
||||
Tryton itself is very flexible in allowing multiple layers of Projects and
|
||||
|
@ -343,6 +373,26 @@ class Project:
|
|||
self.constraint_finish_time.isoformat() or None,
|
||||
}
|
||||
|
||||
def _json(self):
|
||||
'''
|
||||
Serialize the work and returns a dictionary
|
||||
'''
|
||||
rv = {
|
||||
'id': self.id,
|
||||
'displayName': self.name,
|
||||
'type': self.type,
|
||||
'objectType': self.__name__,
|
||||
}
|
||||
if self.type == 'project':
|
||||
rv['url'] = url_for(
|
||||
'project.work.render_project', active_id=self.id
|
||||
)
|
||||
else:
|
||||
rv['url'] = url_for(
|
||||
'project.work.render_task', active_id=self.id
|
||||
)
|
||||
return rv
|
||||
|
||||
@classmethod
|
||||
def rst_to_html(cls):
|
||||
"""
|
||||
|
@ -432,19 +482,19 @@ class Project:
|
|||
|
||||
:param project_id: Project Id of project to fetch.
|
||||
"""
|
||||
project, = cls.search([
|
||||
projects = cls.search([
|
||||
('id', '=', project_id),
|
||||
('type', '=', 'project'),
|
||||
])
|
||||
|
||||
if not project:
|
||||
if not projects:
|
||||
raise abort(404)
|
||||
|
||||
if not project.can_read(request.nereid_user):
|
||||
if not projects[0].can_read(request.nereid_user):
|
||||
# If the user is not allowed to access this project then dont let
|
||||
raise abort(404)
|
||||
|
||||
return project
|
||||
return projects[0]
|
||||
|
||||
@classmethod
|
||||
def get_task(cls, task_id):
|
||||
|
@ -454,19 +504,19 @@ class Project:
|
|||
|
||||
:param task_id: Task Id of project to fetch.
|
||||
"""
|
||||
task, = cls.search([
|
||||
tasks = cls.search([
|
||||
('id', '=', task_id),
|
||||
('type', '=', 'task'),
|
||||
])
|
||||
|
||||
if not task:
|
||||
if not tasks:
|
||||
raise abort(404)
|
||||
|
||||
if not task.parent.can_write(request.nereid_user):
|
||||
if not tasks[0].parent.can_write(request.nereid_user):
|
||||
# If the user is not allowed to access this project then dont let
|
||||
raise abort(403)
|
||||
|
||||
return task
|
||||
return tasks[0]
|
||||
|
||||
@classmethod
|
||||
def get_tasks_by_tag(cls, tag_id):
|
||||
|
@ -504,6 +554,7 @@ class Project:
|
|||
|
||||
POST will create a new project
|
||||
"""
|
||||
Activity = Pool().get('nereid.activity')
|
||||
if not request.nereid_user.is_project_admin():
|
||||
flash("Sorry! You are not allowed to create new projects." +
|
||||
" Contact your project admin for the same.")
|
||||
|
@ -514,6 +565,12 @@ class Project:
|
|||
'name': request.form['name'],
|
||||
'type': 'project',
|
||||
})
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work, %d' % project.id,
|
||||
'verb': 'created_project',
|
||||
'project': project.id,
|
||||
})
|
||||
flash("Project successfully created.")
|
||||
return redirect(
|
||||
url_for('project.work.render_project', project_id=project.id)
|
||||
|
@ -529,6 +586,7 @@ class Project:
|
|||
POST will create a new task
|
||||
"""
|
||||
NereidUser = Pool().get('nereid.user')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
project = self.get_project(self.id)
|
||||
|
||||
|
@ -541,6 +599,9 @@ class Project:
|
|||
'name': request.form['name'],
|
||||
'type': 'task',
|
||||
'comment': request.form.get('description', False),
|
||||
'tags': [('set',
|
||||
request.form.getlist('tags', int)
|
||||
)]
|
||||
}
|
||||
|
||||
constraint_start_time = request.form.get(
|
||||
|
@ -555,6 +616,13 @@ class Project:
|
|||
constraint_finish_time, '%m/%d/%Y')
|
||||
|
||||
task = self.create(data)
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work, %d' % task.id,
|
||||
'verb': 'created_task',
|
||||
'target': 'project.work, %d' % project.id,
|
||||
'project': project.id,
|
||||
})
|
||||
|
||||
email_receivers = [p.email for p in self.all_participants]
|
||||
if request.form.get('assign_to', False):
|
||||
|
@ -582,13 +650,20 @@ class Project:
|
|||
"""
|
||||
Edit the task
|
||||
"""
|
||||
Activity = Pool().get('nereid.activity')
|
||||
task = self.get_task(self.id)
|
||||
|
||||
self.write([task], {
|
||||
'name': request.form.get('name'),
|
||||
'comment': request.form.get('comment')
|
||||
})
|
||||
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work, %d' % task.id,
|
||||
'verb': 'edited_task',
|
||||
'target': 'project.work, %d' % task.parent.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
if request.is_xhr:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
|
@ -688,7 +763,7 @@ class Project:
|
|||
('nereid_user', '=', False)
|
||||
])
|
||||
return render_template(
|
||||
'project/project-permissions.jinja', project=project,
|
||||
'project/permissions.jinja', project=project,
|
||||
invitations=invitations, active_type_name='permissions'
|
||||
)
|
||||
|
||||
|
@ -712,6 +787,7 @@ class Project:
|
|||
"""
|
||||
NereidUser = Pool().get('nereid.user')
|
||||
ProjectInvitation = Pool().get('project.work.invitation')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
if not request.method == 'POST':
|
||||
return abort(404)
|
||||
|
@ -744,6 +820,12 @@ class Project:
|
|||
'participants': [('add', [existing_user[0].id])]
|
||||
}
|
||||
)
|
||||
Activity.create({
|
||||
'actor': existing_user[0].id,
|
||||
'object_': 'project.work, %d' % project.id,
|
||||
'verb': 'joined_project',
|
||||
'project': project.id,
|
||||
})
|
||||
flash_message = "%s has been invited to the project" \
|
||||
% existing_user[0].display_name
|
||||
|
||||
|
@ -775,6 +857,7 @@ class Project:
|
|||
def remove_participant(self, participant_id):
|
||||
"""Remove the participant form project
|
||||
"""
|
||||
Activity = Pool().get('nereid.activity')
|
||||
# Check if user is among the project admins
|
||||
if not request.nereid_user.is_project_admin():
|
||||
flash("Sorry! You are not allowed to remove participants." +
|
||||
|
@ -802,6 +885,13 @@ class Project:
|
|||
self.__class__(rec_id), records_to_update_ids
|
||||
), {'participants': [('unlink', [participant_id])]}
|
||||
)
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'nereid.user, %d' % participant_id,
|
||||
'target': 'project.work, %d' % self.id,
|
||||
'verb': 'removed_participant',
|
||||
'project': self.id,
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
|
@ -955,7 +1045,7 @@ class Project:
|
|||
task.attachments]
|
||||
)
|
||||
return render_template(
|
||||
'project/project-files.jinja', project=project,
|
||||
'project/files.jinja', project=project,
|
||||
active_type_name='files', guess_type=guess_type,
|
||||
other_attachments=other_attachments
|
||||
)
|
||||
|
@ -1003,40 +1093,49 @@ class Project:
|
|||
return '22-END'
|
||||
|
||||
@classmethod
|
||||
def get_calendar_data(cls, domain=None):
|
||||
def get_task_from_work(cls, work):
|
||||
'''
|
||||
Returns task from work
|
||||
|
||||
:param work: Instance of work
|
||||
'''
|
||||
with Transaction().set_context(active_test=False):
|
||||
task, = cls.search([('work', '=', work.id)], limit=1)
|
||||
return task
|
||||
|
||||
@classmethod
|
||||
def get_calendar_data(cls, project=None):
|
||||
"""
|
||||
Returns the calendar data
|
||||
|
||||
:param domain: List of tuple to add to the domain expression
|
||||
"""
|
||||
Timesheet = Pool().get('timesheet.line')
|
||||
ProjectWork = Pool().get('project.work')
|
||||
Employee = Pool().get('company.employee')
|
||||
|
||||
if request.args.get('timesheet_lines_of'):
|
||||
# This request only expects timesheet lines and the request comes
|
||||
# in the format date:employee_id:project_id
|
||||
date, employee_id, project_id = request.args.get('timesheet_lines_of').split(':')
|
||||
domain = [
|
||||
('date', '=', datetime.strptime(date, '%Y-%m-%d').date()),
|
||||
('employee', '=', int(employee_id))
|
||||
]
|
||||
if int(project_id):
|
||||
project = ProjectWork(int(project_id))
|
||||
domain.append(('work.parent', 'child_of', [project.work.id]))
|
||||
lines = Timesheet.search(
|
||||
domain, order=[('date', 'asc'), ('employee', 'asc')]
|
||||
)
|
||||
return jsonify(lines=[
|
||||
render_template(
|
||||
'project/timesheet-line.jinja', line=line,
|
||||
related_task=cls.get_task_from_work(line.work)
|
||||
) \
|
||||
for line in lines[::-1]
|
||||
])
|
||||
|
||||
start, end = cls._get_expected_date_range()
|
||||
|
||||
if domain is None:
|
||||
domain = []
|
||||
domain += [
|
||||
('date', '>=', start),
|
||||
('date', '<=', end),
|
||||
]
|
||||
|
||||
if request.args.get('employee', None) and \
|
||||
request.nereid_user.has_permissions(['project.admin']):
|
||||
domain.append(
|
||||
('employee', '=', request.args.get('employee', None, int))
|
||||
)
|
||||
lines = Timesheet.search(
|
||||
domain, order=[('date', 'asc'), ('employee', 'asc')]
|
||||
)
|
||||
|
||||
hours_by_day_employee = defaultdict(lambda: defaultdict(float))
|
||||
hours_by_week_employee = defaultdict(lambda: defaultdict(float))
|
||||
|
||||
for line in lines:
|
||||
hours_by_day_employee[line.date][line.employee] += line.hours
|
||||
hours_by_week_employee[cls.get_week(line.date.day)] \
|
||||
[line.employee] += line.hours
|
||||
|
||||
day_totals = []
|
||||
color_map = {}
|
||||
colors = cycle([
|
||||
|
@ -1044,36 +1143,55 @@ class Project:
|
|||
'SeaGreen', 'Silver', 'MediumOrchid', 'Olive',
|
||||
'maroon', 'PaleTurquoise'
|
||||
])
|
||||
day_totals_append = day_totals.append # Performance speedup
|
||||
for date, employee_hours in hours_by_day_employee.iteritems():
|
||||
for employee, hours in employee_hours.iteritems():
|
||||
day_totals_append({
|
||||
'id': '%s.%s' % (date, employee.id),
|
||||
'title': '%s (%dh %dm)' % (
|
||||
employee.name, hours, (hours * 60) % 60
|
||||
),
|
||||
'start': date.isoformat(),
|
||||
'color': color_map.setdefault(employee, colors.next()),
|
||||
})
|
||||
query = '''SELECT
|
||||
timesheet_line.employee,
|
||||
timesheet_line.date,
|
||||
'''
|
||||
if project:
|
||||
query += 'project_work.id AS project,'
|
||||
else:
|
||||
query += '0 AS project,'
|
||||
query += '''SUM(timesheet_line.hours) AS sum
|
||||
FROM timesheet_line
|
||||
JOIN timesheet_work ON timesheet_work.id = timesheet_line.work AND timesheet_work.parent IS NOT NULL
|
||||
JOIN project_work ON project_work.work = timesheet_work.parent
|
||||
WHERE
|
||||
timesheet_line.date >= %s AND
|
||||
timesheet_line.date <= %s
|
||||
'''
|
||||
qargs = [start, end]
|
||||
if project:
|
||||
qargs.append(project.id)
|
||||
query += 'AND project_work.id = %s'
|
||||
|
||||
def get_task_from_work(work):
|
||||
'''
|
||||
Returns task from work
|
||||
if request.args.get('employee', None) and \
|
||||
request.nereid_user.has_permissions(['project.admin']):
|
||||
qargs.append(request.args.get('employee', None, int))
|
||||
query += 'AND timesheet_line.employee = %s'
|
||||
|
||||
:param work: Instance of work
|
||||
'''
|
||||
with Transaction().set_context(active_test=False):
|
||||
task, = cls.search([('work', '=', work.id)], limit=1)
|
||||
return task
|
||||
query += '''
|
||||
GROUP BY
|
||||
timesheet_line.employee,
|
||||
timesheet_line.date
|
||||
'''
|
||||
if project:
|
||||
query += ',project_work.id'
|
||||
|
||||
reverse_lines = lines[::-1]
|
||||
lines = [
|
||||
render_template(
|
||||
'project/timesheet-line.jinja', line=line,
|
||||
related_task=get_task_from_work(line.work)
|
||||
) \
|
||||
for line in reverse_lines
|
||||
]
|
||||
Transaction().cursor.execute(query, qargs)
|
||||
raw_data = Transaction().cursor.fetchall()
|
||||
|
||||
hours_by_week_employee = defaultdict(lambda: defaultdict(float))
|
||||
for employee_id, date, project_id, hours in raw_data:
|
||||
employee = Employee(employee_id)
|
||||
day_totals.append({
|
||||
'id': '%s:%s:%s' % (date, employee.id, project_id),
|
||||
'title': '%s (%dh %dm)' % (
|
||||
employee.name, hours, (hours * 60) % 60
|
||||
),
|
||||
'start': date.isoformat(),
|
||||
'color': color_map.setdefault(employee, colors.next()),
|
||||
})
|
||||
hours_by_week_employee[cls.get_week(date.day)][employee] += hours
|
||||
|
||||
total_by_employee = defaultdict(float)
|
||||
for employee_hours in hours_by_week_employee.values():
|
||||
|
@ -1084,7 +1202,7 @@ class Project:
|
|||
'project/work-week.jinja', data_by_week=hours_by_week_employee,
|
||||
total_by_employee=total_by_employee
|
||||
)
|
||||
return jsonify(day_totals=day_totals, lines=lines, work_week=work_week)
|
||||
return jsonify(day_totals=day_totals, lines=[], work_week=work_week)
|
||||
|
||||
@classmethod
|
||||
@login_required
|
||||
|
@ -1121,7 +1239,8 @@ class Project:
|
|||
'series': ['%.2f' % hours_by_day[day] for day in days]
|
||||
})
|
||||
|
||||
def get_comparison_data(self):
|
||||
@classmethod
|
||||
def get_comparison_data(cls):
|
||||
"""
|
||||
Compare the performance of people
|
||||
"""
|
||||
|
@ -1211,7 +1330,8 @@ class Project:
|
|||
series=series + additional
|
||||
)
|
||||
|
||||
def get_gantt_data(self):
|
||||
@classmethod
|
||||
def get_gantt_data(cls):
|
||||
"""
|
||||
Get gantt data for the last 1 month.
|
||||
"""
|
||||
|
@ -1248,7 +1368,7 @@ class Project:
|
|||
employee = employees.get(employee_id)
|
||||
gantt_data_append({
|
||||
'name': employee and employee.name or 'Ghost',
|
||||
'desc': '',
|
||||
'desc': '',
|
||||
'values': values,
|
||||
})
|
||||
gantt_data = sorted(gantt_data, key=lambda item: item['name'].lower())
|
||||
|
@ -1276,16 +1396,17 @@ class Project:
|
|||
end_date=today
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@login_required
|
||||
@permissions_required(['project.admin'])
|
||||
def render_global_gantt(self):
|
||||
def render_global_gantt(cls):
|
||||
"""
|
||||
Renders a global gantt
|
||||
"""
|
||||
Employee = Pool().get('company.employee')
|
||||
|
||||
if request.is_xhr:
|
||||
return self.get_gantt_data()
|
||||
return cls.get_gantt_data()
|
||||
employees = Employee.search([])
|
||||
return render_template(
|
||||
'project/global-gantt.jinja', employees=employees
|
||||
|
@ -1345,9 +1466,7 @@ class Project:
|
|||
if p.employee
|
||||
]
|
||||
if request.is_xhr:
|
||||
return cls.get_calendar_data(
|
||||
[('work.parent', 'child_of', [project.work.id])]
|
||||
)
|
||||
return cls.get_calendar_data(project)
|
||||
return render_template(
|
||||
'project/timesheet.jinja', project=project,
|
||||
active_type_name="timesheet", employees=employees
|
||||
|
@ -1418,7 +1537,7 @@ class Project:
|
|||
)
|
||||
|
||||
return render_template(
|
||||
'project/project-plan.jinja', project=project,
|
||||
'project/plan.jinja', project=project,
|
||||
active_type_name='plan'
|
||||
)
|
||||
|
||||
|
@ -1479,9 +1598,20 @@ class Project:
|
|||
raise abort(404)
|
||||
|
||||
attached_file = request.files["file"]
|
||||
resource = '%s,%d' % (cls.__name__, work.id)
|
||||
|
||||
if Attachment.search([
|
||||
('name', '=', attached_file.filename),
|
||||
('resource', '=', resource)
|
||||
]):
|
||||
flash(
|
||||
'File already exists with same name, please choose another ' +
|
||||
'file or rename this file to upload !!'
|
||||
)
|
||||
return redirect(request.referrer)
|
||||
|
||||
data = {
|
||||
'resource': '%s,%d' % (cls.__name__, work.id),
|
||||
'resource': resource,
|
||||
'description': request.form.get('description', '')
|
||||
}
|
||||
|
||||
|
@ -1519,6 +1649,7 @@ class Project:
|
|||
"""
|
||||
History = Pool().get('project.work.history')
|
||||
TimesheetLine = Pool().get('timesheet.line')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
task = cls.get_task(task_id)
|
||||
|
||||
|
@ -1554,6 +1685,7 @@ class Project:
|
|||
current_participant_ids:
|
||||
new_participant_ids.append(new_assignee_id)
|
||||
|
||||
|
||||
if task_changes:
|
||||
# Only write change if anything has really changed
|
||||
cls.write([task], task_changes)
|
||||
|
@ -1567,7 +1699,13 @@ class Project:
|
|||
else:
|
||||
# Just comment, no update to task
|
||||
comment = History.create(history_data)
|
||||
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work.history, %d' % comment.id,
|
||||
'verb': 'updated_task',
|
||||
'target': 'project.work, %d' % task.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
|
||||
if request.nereid_user.id not in current_participant_ids:
|
||||
# Add the user to the participants if not already in the list
|
||||
|
@ -1586,11 +1724,18 @@ class Project:
|
|||
|
||||
hours = request.form.get('hours', None, type=float)
|
||||
if hours and request.nereid_user.employee:
|
||||
TimesheetLine.create({
|
||||
timesheet_line = TimesheetLine.create({
|
||||
'employee': request.nereid_user.employee.id,
|
||||
'hours': hours,
|
||||
'work': task.work.id
|
||||
})
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'timesheet.line, %d' % timesheet_line.id,
|
||||
'verb': 'reported_time',
|
||||
'target': 'project.work, %d' % task.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
|
||||
# Send the email since all thats required is done
|
||||
comment.send_mail()
|
||||
|
@ -1615,11 +1760,19 @@ class Project:
|
|||
:param task_id: ID of task
|
||||
:param tag_id: ID of tag
|
||||
"""
|
||||
Activity = Pool().get('nereid.activity')
|
||||
task = cls.get_task(task_id)
|
||||
|
||||
cls.write(
|
||||
[task], {'tags': [('add', [tag_id])]}
|
||||
)
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work.tag, %d' % tag_id,
|
||||
'verb': 'added_tag_to_task',
|
||||
'target': 'project.work, %d' % task.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
|
||||
if request.method == 'POST':
|
||||
flash('Tag added to task %s' % task.name)
|
||||
|
@ -1637,11 +1790,19 @@ class Project:
|
|||
:param task_id: ID of task
|
||||
:param tag_id: ID of tag
|
||||
"""
|
||||
Activity = Pool().get('nereid.activity')
|
||||
task = cls.get_task(task_id)
|
||||
|
||||
cls.write(
|
||||
[task], {'tags': [('unlink', [tag_id])]}
|
||||
)
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work, %d' % task.id,
|
||||
'verb': 'removed_tag_from_task',
|
||||
'target': 'project.work, %d' % task.parent.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
|
||||
if request.method == 'POST':
|
||||
flash('Tag removed from task %s' % task.name)
|
||||
|
@ -1700,6 +1861,7 @@ class Project:
|
|||
:param task_id: Id of Task
|
||||
"""
|
||||
NereidUser = Pool().get('nereid.user')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
task = cls.get_task(task_id)
|
||||
|
||||
|
@ -1714,6 +1876,13 @@ class Project:
|
|||
'participants': [('add', [new_assignee.id])]
|
||||
})
|
||||
task.history[-1].send_mail()
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work, %d' % task.id,
|
||||
'verb': 'assigned_task_to',
|
||||
'target': 'nereid.user, %d' % new_assignee.id,
|
||||
'project': task.parent.id,
|
||||
})
|
||||
if request.is_xhr:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
|
@ -1852,6 +2021,20 @@ class Tag(ModelSQL, ModelView):
|
|||
'''
|
||||
return "#999"
|
||||
|
||||
def _json(self):
|
||||
'''
|
||||
Serialize the tag and returns a dictionary.
|
||||
'''
|
||||
return {
|
||||
"url": url_for(
|
||||
'project.work.render_task_list', project_id=self.project.id,
|
||||
state="opened", tag=self.id
|
||||
),
|
||||
"objectType": self.__name__,
|
||||
"id": self.id,
|
||||
"displayName": self.name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@login_required
|
||||
def create_tag(cls, project_id):
|
||||
|
@ -1861,6 +2044,7 @@ class Tag(ModelSQL, ModelView):
|
|||
:params project_id: Project id for which need to be created
|
||||
"""
|
||||
Project = Pool().get('project.work')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
project = Project.get_project(project_id)
|
||||
|
||||
|
@ -1871,11 +2055,18 @@ class Tag(ModelSQL, ModelView):
|
|||
return redirect(request.referrer)
|
||||
|
||||
if request.method == 'POST':
|
||||
cls.create({
|
||||
tag = cls.create({
|
||||
'name': request.form['name'],
|
||||
'color': request.form['color'],
|
||||
'project': project.id
|
||||
})
|
||||
Activity.create({
|
||||
'actor': request.nereid_user.id,
|
||||
'object_': 'project.work.tag, %d' % tag.id,
|
||||
'verb': 'created_tag',
|
||||
'target': 'project.work, %d' % project.id,
|
||||
'project': project.id,
|
||||
})
|
||||
|
||||
flash("Successfully created tag")
|
||||
return redirect(request.referrer)
|
||||
|
@ -1930,7 +2121,7 @@ class TaskTags(ModelSQL):
|
|||
|
||||
# Migration
|
||||
if table.table_exist(cursor, 'project_work_tag_rel'):
|
||||
table.table_exist(
|
||||
table.table_rename(
|
||||
cursor, 'project_work_tag_rel', 'project_work-project_work_tag'
|
||||
)
|
||||
|
||||
|
@ -1990,6 +2181,19 @@ class ProjectHistory(ModelSQL, ModelView):
|
|||
'''
|
||||
return datetime.utcnow()
|
||||
|
||||
def _json(self):
|
||||
'''
|
||||
Serialize the history and returns a dictionary.
|
||||
'''
|
||||
return {
|
||||
"url": url_for(
|
||||
'project.work.render_task_list', comment=self.comment.id
|
||||
),
|
||||
"objectType": self.__name__,
|
||||
"id": self.id,
|
||||
"displayName": self.display_name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_history_line(cls, project, changed_values):
|
||||
"""
|
||||
|
@ -2110,7 +2314,8 @@ class ProjectWorkCommit(ModelSQL, ModelView):
|
|||
commit_url = fields.Char('Commit URL', required=True)
|
||||
commit_id = fields.Char('Commit Id', required=True)
|
||||
|
||||
def commit_github_hook_handler(self):
|
||||
@classmethod
|
||||
def commit_github_hook_handler(cls):
|
||||
"""
|
||||
Handle post commit posts from GitHub
|
||||
See https://help.github.com/articles/post-receive-hooks
|
||||
|
@ -2143,7 +2348,7 @@ class ProjectWorkCommit(ModelSQL, ModelView):
|
|||
commit_timestamp = local_commit_time.astimezone(
|
||||
dateutil.tz.tzutc()
|
||||
)
|
||||
self.create({
|
||||
cls.create({
|
||||
'commit_timestamp': commit_timestamp,
|
||||
'project': project,
|
||||
'nereid_user': nereid_users[0].id,
|
||||
|
@ -2155,7 +2360,8 @@ class ProjectWorkCommit(ModelSQL, ModelView):
|
|||
})
|
||||
return 'OK'
|
||||
|
||||
def commit_bitbucket_hook_handler(self):
|
||||
@classmethod
|
||||
def commit_bitbucket_hook_handler(cls):
|
||||
"""
|
||||
Handle post commit posts from bitbucket
|
||||
See https://confluence.atlassian.com/display/BITBUCKET/POST+Service+Management
|
||||
|
@ -2187,7 +2393,7 @@ class ProjectWorkCommit(ModelSQL, ModelView):
|
|||
commit_timestamp = local_commit_time.astimezone(
|
||||
dateutil.tz.tzutc()
|
||||
)
|
||||
self.create({
|
||||
cls.create({
|
||||
'commit_timestamp': commit_timestamp,
|
||||
'project': project,
|
||||
'nereid_user': nereid_users[0].id,
|
||||
|
@ -2217,6 +2423,7 @@ def invitation_new_user_handler(nereid_user_id):
|
|||
Invitation = Pool().get('project.work.invitation')
|
||||
Project = Pool().get('project.work')
|
||||
NereidUser = Pool().get('nereid.user')
|
||||
Activity = Pool().get('nereid.activity')
|
||||
|
||||
except KeyError:
|
||||
# Just return silently. This KeyError is cause if the module is not
|
||||
|
@ -2266,3 +2473,20 @@ def invitation_new_user_handler(nereid_user_id):
|
|||
'participants': [('add', [nereid_user_id])]
|
||||
}
|
||||
)
|
||||
Activity.create({
|
||||
'actor': nereid_user_id,
|
||||
'object_': 'project.work, %d' % invitation.project.id,
|
||||
'verb': 'joined_project',
|
||||
'project': invitation.project.id,
|
||||
})
|
||||
|
||||
|
||||
class Activity:
|
||||
'''
|
||||
Nereid user activity
|
||||
'''
|
||||
__name__ = "nereid.activity"
|
||||
|
||||
project = fields.Many2One(
|
||||
'project.work', 'Project', domain=[('type', '=', 'project')]
|
||||
)
|
||||
|
|
43
project.xml
|
@ -37,6 +37,49 @@ of this repository contains the full copyright notices and license terms. -->
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="timesheet_by_employee_by_day_form">
|
||||
<field name="model">timesheet_by_employee_by_day</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<form string="Update Gantt Chart">
|
||||
<label name="employee"/>
|
||||
<field name="employee"/>
|
||||
<label name="date"/>
|
||||
<field name="date"/>
|
||||
<label name="hours"/>
|
||||
<field name="hours"/>
|
||||
</form>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="nereid.activity.allowed_model"
|
||||
id="activity_allowed_model_work">
|
||||
<field name="name">ProjectWork</field>
|
||||
<field name="model" search="[('model', '=', 'project.work')]"/>
|
||||
</record>
|
||||
<record model="nereid.activity.allowed_model"
|
||||
id="activity_allowed_model_work_tag">
|
||||
<field name="name">ProjectWorkTag</field>
|
||||
<field name="model" search="[('model', '=', 'project.work.tag')]"/>
|
||||
</record>
|
||||
<record model="nereid.activity.allowed_model"
|
||||
id="activity_allowed_model_nereid_user">
|
||||
<field name="name">NereidUser</field>
|
||||
<field name="model" search="[('model', '=', 'nereid.user')]"/>
|
||||
</record>
|
||||
<record model="nereid.activity.allowed_model"
|
||||
id="activity_allowed_model_work_history">
|
||||
<field name="name">ProjectWorkHistory</field>
|
||||
<field name="model" search="[('model', '=', 'project.work.history')]"/>
|
||||
</record>
|
||||
<record model="nereid.activity.allowed_model"
|
||||
id="activity_allowed_model_timesheet_line">
|
||||
<field name="name">TimesheetLine</field>
|
||||
<field name="model" search="[('model', '=', 'timesheet.line')]"/>
|
||||
</record>
|
||||
|
||||
<record id="permission_project_admin" model="nereid.permission">
|
||||
<field name="name">Project Admin</field>
|
||||
<field name="value">project.admin</field>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
body{
|
||||
cursor: default;
|
||||
}
|
||||
a{
|
||||
cursor: pointer;
|
||||
}
|
||||
.tasks-list .ui-sortable-placeholder, .iteration-block .ui-sortable-placeholder{
|
||||
border: 2px dashed #999;
|
||||
visibility: visible !important;
|
||||
height: 40px !important;
|
||||
background: #ddd;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tasks-list .ui-sortable-placeholder *, .iteration-block .ui-sortable-placeholder *{
|
||||
visibility: hidden;
|
||||
}
|
||||
#task-list-Backlog div.task-item div.task-details, .state-Backlog{
|
||||
border-left: 6px #999 solid;
|
||||
}
|
||||
#task-list-Planning div.task-item div.task-details, .state-Planning{
|
||||
border-left: 6px #378DF0 solid;
|
||||
}
|
||||
#task-list-In_Progress div.task-item div.task-details, .state-In_Progress{
|
||||
border-left: 6px #7EA629 solid;
|
||||
}
|
||||
#task-list-Review div.task-item div.task-details, .state-Review{
|
||||
border-left: 6px #ED811D solid;
|
||||
}
|
||||
|
||||
div.task-item div.task-details {
|
||||
font-size: 13px;
|
||||
border-radius:5px;
|
||||
padding:6px;
|
||||
box-shadow: #999 0px 0px 5px;
|
||||
background: #fefefe;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
div.task-item div.task-details.delay {
|
||||
box-shadow: #D93030 0px 0px 5px;
|
||||
border-left: 6px #D93030 solid !important;
|
||||
background: #FFF0F0;
|
||||
}
|
||||
div.task-item a, div.task-item a:hover {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
div.task-item .size{
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-shadow: #999 0 0 3px;
|
||||
}
|
||||
div.task-item .icon-hide{
|
||||
opacity: 0;
|
||||
padding-left: 5px;
|
||||
}
|
||||
div.task-item:not(.done):hover .icon-hide{
|
||||
opacity: 1;
|
||||
}
|
||||
a.action-tag-btn:hover span.label{
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
div.task-item .icon{
|
||||
display: block;
|
||||
padding: 0 5px;
|
||||
}
|
||||
div.task-item .task-footer{
|
||||
padding-top: 5px;
|
||||
}
|
||||
div.task-item .tag{
|
||||
background: #efefef;
|
||||
}
|
||||
div.task-item .tag:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
div.task-item.done div.task-details{
|
||||
background: #EFEFEF;
|
||||
}
|
||||
div.task-item.done div.task-details a.task-name{
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
div.popover-content a{
|
||||
color: inherit;
|
||||
}
|
||||
div.popover-content a:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.iteration-block{
|
||||
border: #999 1px solid;
|
||||
border-radius: 5px;
|
||||
box-shadow: #000 0 0 5px;
|
||||
padding: 10px;
|
||||
height: 400px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.clear{
|
||||
clear: both;
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<link href="{{ STATIC }}css/bootstrap.css" rel="stylesheet">
|
||||
<link href="{{ STATIC }}css/font-awesome.css" rel="stylesheet">
|
||||
<link href="{{ STATIC }}css/custom.css" rel="stylesheet">
|
||||
<link href="{{ STATIC }}css/bootstrap-responsive.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ STATIC }}css/jquery.meow.css" type="text/css" media="screen" title="no title" charset="utf-8">
|
||||
<link href="{{ STATIC }}css/chosen.css" type="text/css" rel="stylesheet" />
|
||||
|
@ -26,6 +27,16 @@
|
|||
<link href="{{ STATIC }}css/prettify.css" type="text/css" rel="stylesheet" />
|
||||
<link href="{{ STATIC }}css/ui-lightness/jquery-ui-1.8.21.custom.css" type="text/css" rel="stylesheet" />
|
||||
<script type="text/javascript" src="{{ STATIC }}js/prettify/prettify.js"></script>
|
||||
<script type="text/template" id="popover2-template">
|
||||
<div class="popover" onmouseover="$(this).mouseleave(function() {$(this).hide(); });">
|
||||
<div class="arrow"></div>
|
||||
<div class="popover-inner">
|
||||
<div class="popover-content">
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
|
@ -46,9 +57,15 @@
|
|||
|
||||
// Activate toltips
|
||||
$("a[rel='tooltip']").tooltip();
|
||||
$("img[rel='tooltip']").tooltip();
|
||||
$("a[rel='popover2']").popover({
|
||||
template: $("#popover2-template").html(),
|
||||
});
|
||||
$("a[rel='popover']").popover()
|
||||
|
||||
// Change all timeago dates
|
||||
jQuery("abbr.timeago").timeago();
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
@ -107,6 +124,7 @@
|
|||
|
||||
<!-- Other JS -->
|
||||
<script src="{{ STATIC }}js/jquery.min.js"></script>
|
||||
<script src="{{ STATIC }}js/jquery-ui.js"></script>
|
||||
<script src="{{ STATIC }}js/bootstrap.js"></script>
|
||||
<script src="{{ STATIC }}js/chosen.jquery.min.js"></script>
|
||||
<script src="{{ STATIC }}js/jquery.timeago.js"></script>
|
||||
|
|
|
@ -26,88 +26,128 @@
|
|||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
|
||||
|
||||
|
||||
{% macro render_task(task, assigned_to_picture=True, show_effort=True, show_hours=True, show_attachments=True, show_watch_toggle=True, show_project_tag=False, show_project=True, show_progress_state=True) %}
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner"><div class="container">
|
||||
{% if assigned_to_picture and task.assigned_to %}
|
||||
<ul class="nav">
|
||||
<li class="text-align: center">
|
||||
<a rel="tooltip" title="Assigned to {{ task.assigned_to.display_name }}" style="padding: 5px 10px 5px 10px;margin-top:4px;"><img class="img-circle" src="{{ task.assigned_to.get_profile_picture(size="25") }}"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<a class="brand" href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" rel="tooltip" title="created by {{ task.created_by.display_name }}" style="margin-left:-10px; padding: 0 0 0 0; font-size:16px;{% if task.constraint_finish_time and task.constraint_finish_time < datetime.datetime.utcnow() %}color:#BD362F{% endif %}">
|
||||
|
||||
<p class="navbar-text">
|
||||
{% if task.state == "done" %}<del>{% endif %}
|
||||
#{{ task.id }} {{ task.name|truncate(50) }}
|
||||
{% if task.state == "done" %}</del>{% endif %}
|
||||
|
||||
{% for tag in task.tags %}
|
||||
<button class="btn btn-mini disabled btn-{{ tag.color }}">{{ tag.name }}</button>
|
||||
{% endfor %}
|
||||
{#% if task.constraint_start_time %} <span class="label label-info"> Start by {{ task.constraint_start_time|dateformat }}</span>{% endif %#}
|
||||
{% if task.constraint_finish_time %} <span class="label label-info"> {{ task.constraint_finish_time|dateformat }}</span> {% endif %}
|
||||
|
||||
{% if show_progress_state %}
|
||||
{% set state = "Done" if task.state == "done" else task.progress_state %}
|
||||
<span class="label label-{{ state_color_css(state) }}">{{ state }}</span>
|
||||
{% endif %}
|
||||
{% if show_project_tag %}
|
||||
<span class="label">{{ task.parent and task.parent.name or '' }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<ul class="nav pull-right">
|
||||
{% if show_project and not project %}
|
||||
<li>
|
||||
<sup><a class="btn btn-mini pull-right" href="{{ url_for('project.work.render_project', project_id=task.parent.id) }}">{{ task.parent.name }}</a></sup>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if show_attachments and task.attachments %}
|
||||
<li class="divider-vertical"></li>
|
||||
<li>
|
||||
<a href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" rel="tooltip" title="Attachments">
|
||||
<i class="icon-paper-clip"></i> {{ task.attachments|length }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_effort and task.effort %}
|
||||
<li class="divider-vertical"></li>
|
||||
<li>
|
||||
<a rel="tooltip" title="Estimated Effort"><span class="badge badge-warning">{{ task.effort|float_to_time }}</span></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_hours and task.hours %}
|
||||
<li class="divider-vertical"></li>
|
||||
<li>
|
||||
<a rel="tooltip" title="Actual Effort so far"><span class="badge badge-info">{{ task.hours|float_to_time }}</span></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_watch_toggle %}
|
||||
<li class="divider-vertical"></li>
|
||||
<li {% if request.nereid_user in task.participants %}class="active"{% endif %}>
|
||||
<a class="watch" data-url="{{ url_for('project.work.watch', task_id=task.id) }}" {% if request.nereid_user in task.participants %}style="display:none"{% endif %} rel="tooltip" title="{{ _('Watch') }}"><i class="icon-eye-open"></i></a>
|
||||
<a class="unwatch" data-url="{{ url_for('project.work.unwatch', task_id=task.id) }}"{% if request.nereid_user not in task.participants %}style="display:none"{% endif %} rel="tooltip" title="{{ _('Unwatch') }}"><i class="icon-eye-close"></i></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
</div></div>
|
||||
</div>
|
||||
{#<p><small>created by {{ task.created_by.name }} <abbr class="timeago" title="{{ request.nereid_user.aslocaltime(task.create_date) }}">{{ request.nereid_user.aslocaltime(task.create_date) }}</abbr></small></p>#}
|
||||
{% macro render_task(task, assigned_to_picture=True, show_project_tag=False) %}
|
||||
<div class="task-item {% if task.state == 'done' %}done{% endif %}" update-url="{{ url_for("project.work.update_task", task_id=task.id) }}" state="{{ task.state }}">
|
||||
<div class='task-details {% if task.constraint_finish_time and task.constraint_finish_time < datetime.datetime.utcnow() %}delay{% endif %}'>
|
||||
<table cellSpacing="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<a class="task-name" href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" rel="tooltip" title="created by {{ task.created_by.display_name }}">
|
||||
<span class="muted">#{{ task.id }}</span> {{ task.name|truncate(50) }}
|
||||
</a>
|
||||
<a class="icon-tag add-tags" title="Add Tag" rel="popover" data-placement="bottom" data-html="true" data-animation="true" data-content='
|
||||
{% for new_tag in task.parent.tags_for_projects %}
|
||||
{% if new_tag not in task.tags %}
|
||||
<div>
|
||||
<a class="action-tag-btn icon-plus-sign" id="{{ new_tag.id }}"
|
||||
data-url="{{ url_for('project.work.add_tag', task_id=task.id, tag_id=new_tag.id) }}"></a>
|
||||
<span class="label label-{{ new_tag.color if new_tag.color != 'danger' else 'important' }}">
|
||||
{{ new_tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<a class="action-tag-btn icon-remove-sign" id="{{ new_tag.id }}"
|
||||
data-url="{{ url_for('project.work.remove_tag', task_id=task.id, tag_id=new_tag.id) }}"></a>
|
||||
<span class="label label-{{ new_tag.color if new_tag.color != 'danger' else 'important' }}">
|
||||
{{ new_tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div>No Tag to add</div>
|
||||
{% endfor %}'></a>
|
||||
{% for tag in task.tags %}
|
||||
<a class="" href="{{ url_for('project.work.render_task_list', project_id=task.parent.id, tag=tag.id) }}">{{ tag.name }}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td valign="top" width="20">
|
||||
<a class="icon-edit icon-hide" href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" title="Edit"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="task-footer" colspan="2">
|
||||
{% if assigned_to_picture and task.assigned_to %}
|
||||
<img title="{{ task.assigned_to.name }}" src="{{ task.assigned_to.get_profile_picture(size="20", https=True) }}" class="img-circle">
|
||||
{% endif %}
|
||||
{% if show_project_tag %}
|
||||
<span class="label">{{ task.parent and task.parent.name or '' }}</span>
|
||||
{% endif %}
|
||||
{% if task.point_estimation %}
|
||||
<span class="size icon pull-right" title="Size">{{ task.point_estimation }}</span>
|
||||
{% endif %}
|
||||
<a href="#" class="icon icon-group pull-right" rel="popover2" data-placement="top" data-html="true" data-trigger="hover" data-animation="true" data-content="
|
||||
{% for participant in task.all_participants %}
|
||||
<div><img src='{{ participant.get_profile_picture(size="20", https=True) }}'> {% if participant.id == task.assigned_to.id %}*{% endif %} {{ participant.display_name }}</div>
|
||||
{% endfor %}"></a>
|
||||
<a href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" class="icon icon-comments-alt pull-right">{{ task.history|length }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_task_tree(task, assigned_to_picture=True, show_project_tag=False) %}
|
||||
{% if task.type == "task" %}
|
||||
<div class="task-item task-tree-item {% if task.state == 'done' %}done{% endif %}" update-url="{{ url_for("project.work.update_task", task_id=task.id) }}" state="{{ task.state }}" task_id="{{ task.id }}">
|
||||
<div class='task-details state-{{ task.progress_state|replace(' ', '_') }} {% if task.constraint_finish_time and task.constraint_finish_time < datetime.datetime.utcnow() %}delay{% endif %}'>
|
||||
<table cellSpacing="0" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
{% if assigned_to_picture and task.assigned_to %}
|
||||
<img title="{{ task.assigned_to.name }}" src="{{ task.assigned_to.get_profile_picture(size="20", https=True) }}" class="img-circle">
|
||||
{% endif %}
|
||||
{% if show_project_tag %}
|
||||
<span class="label">{{ task.parent and task.parent.name or '' }}</span>
|
||||
{% endif %}
|
||||
{% if task.point_estimation %}
|
||||
<span class="size icon pull-right" title="Size">{{ task.point_estimation }}</span>
|
||||
{% endif %}
|
||||
<a href="#" class="icon icon-group pull-right" rel="popover2" data-placement="top" data-html="true" data-trigger="hover" data-animation="true" data-content="
|
||||
{% for participant in task.all_participants %}
|
||||
<div><img src='{{ participant.get_profile_picture(size="20", https=True) }}'> {% if participant.id == task.assigned_to.id %}*{% endif %} {{ participant.display_name }}</div>
|
||||
{% endfor %}"></a>
|
||||
<a href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" class="icon icon-comments-alt pull-right">{{ task.history|length }}</a>
|
||||
|
||||
<a class="task-name" href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" rel="tooltip" title="created by {{ task.created_by.display_name }}">
|
||||
<span class="muted">#{{ task.id }}</span> {{ task.name|truncate(70) }}
|
||||
</a>
|
||||
<a class="icon-tag add-tags" title="Add Tag" rel="popover" data-placement="bottom" data-html="true" data-animation="true" data-content='
|
||||
{% for new_tag in task.parent.tags_for_projects %}
|
||||
{% if new_tag not in task.tags %}
|
||||
<div>
|
||||
<a class="action-tag-btn icon-plus-sign" id="{{ new_tag.id }}"
|
||||
data-url="{{ url_for('project.work.add_tag', task_id=task.id, tag_id=new_tag.id) }}"></a>
|
||||
<span class="label label-{{ new_tag.color if new_tag.color != 'danger' else 'important' }}">
|
||||
{{ new_tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<a class="action-tag-btn icon-remove-sign" id="{{ new_tag.id }}"
|
||||
data-url="{{ url_for('project.work.remove_tag', task_id=task.id, tag_id=new_tag.id) }}"></a>
|
||||
<span class="label label-{{ new_tag.color if new_tag.color != 'danger' else 'important' }}">
|
||||
{{ new_tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div>No Tag to add</div>
|
||||
{% endfor %}'></a>
|
||||
{% for tag in task.tags %}
|
||||
<a class="" href="{{ url_for('project.work.render_task_list', project_id=task.parent.id, tag=tag.id) }}">{{ tag.name }}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td valign="top" width="20">
|
||||
<a class="icon-edit icon-hide" href="{{ url_for('project.work.render_task', project_id=task.parent.id, task_id=task.id) }}" title="Edit"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field(field, class_="") %}
|
||||
<div class="clearfix {% if field.errors %}error{% endif %}">
|
||||
|
|
|
@ -20,18 +20,18 @@
|
|||
<div class="pagination">
|
||||
<ul class="span12">
|
||||
{% for state, state_name in states %}
|
||||
<li class="span3"><a class="span12">{{ state_name }} ({{ tasks_by_state[state]|length }})</a></li>
|
||||
<li class="span3"><a class="span12">{{ state_name }} ({{ tasks_by_state[state]|length }})</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
{% for state, state_name in states %}
|
||||
<div class="span3">
|
||||
{% for task in tasks_by_state[state] %}
|
||||
{{ render_task(task, assigned_to_picture=False, show_effort=False, show_hours=False, show_attachments=False, show_watch_toggle=False, show_project_tag=True, show_project=False, show_progress_state=False) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="span3 tasks-list" progress_state="{{ state }}" id="task-list-{{ state|replace(' ', '_') }}">
|
||||
{% for task in tasks_by_state[state] %}
|
||||
{{ render_task(task, assigned_to_picture=True, show_project_tag=True) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,6 +42,50 @@
|
|||
{{ super() }}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$( ".tasks-list" ).sortable({
|
||||
connectWith: ".tasks-list",
|
||||
items: "div.task-item:not(.done)",
|
||||
opacity: 0.7,
|
||||
revert: true,
|
||||
delay: 150,
|
||||
})
|
||||
.disableSelection()
|
||||
.on( "sortreceive", function(event, ui){
|
||||
$.ajax({
|
||||
url: ui.item.attr('update-url'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
progress_state: ui.item.closest("div.tasks-list").attr("progress_state"),
|
||||
state: ui.item.attr('state'),
|
||||
comment: '',
|
||||
},
|
||||
})
|
||||
.always(function(data, textStatus){
|
||||
if(textStatus == "error" || !data.success){
|
||||
alert("Update Failed!")
|
||||
location.reload();
|
||||
}
|
||||
else{
|
||||
$.meow({
|
||||
title: 'Success',
|
||||
message: 'Successfully updated task status.',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
$("a.add-tags").click(function(e){
|
||||
e.preventDefault();
|
||||
$('a.action-tag-btn').click(function(e){
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: $(this).attr("data-url"),
|
||||
type: 'POST',
|
||||
})
|
||||
.done(function(){
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
$('a.log-hours').click(function() {
|
||||
$(this).parents("form").find("tr.hours-input").show();
|
||||
$(this).parents("form").find("tr.hours-input input").focus();
|
||||
|
|
|
@ -47,10 +47,10 @@
|
|||
<div class="tab-pane fade active in" id="tab-calendar">
|
||||
<div id='calendar'></div>
|
||||
</div>
|
||||
<div class="tab-pane fade active in" id="tab-timesheet-lines">
|
||||
<div class="tab-pane fade" id="tab-timesheet-lines">
|
||||
<div id='timesheet-lines'></div>
|
||||
</div>
|
||||
<div class="tab-pane fade active in" id="tab-workweek-lines">
|
||||
<div class="tab-pane fade" id="tab-workweek-lines">
|
||||
<div id='workweek-lines'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +66,6 @@
|
|||
$(document).ready(function(){
|
||||
var fetchFromTryton = function(start, end, callback, event_type) {
|
||||
$("img#loading").show();
|
||||
$("div#timesheet-lines").html('loading...');
|
||||
$("div#workweek-lines").html('loading...');
|
||||
$.ajax({
|
||||
url: "{{ url_for('project.work.render_global_timesheet') }}",
|
||||
|
@ -100,6 +99,28 @@
|
|||
var load_calendar = function() {
|
||||
$('#calendar').fullCalendar({
|
||||
header: {right: 'today month,basicWeek prev,next'},
|
||||
eventClick: function(calEvent, jsEvent, view) {
|
||||
$("img#loading").show();
|
||||
$("div#timesheet-lines").html('loading...');
|
||||
$('#myTab a:last').tab('show');
|
||||
$.ajax({
|
||||
url: "{{ url_for('project.work.render_global_timesheet') }}",
|
||||
data: {
|
||||
timesheet_lines_of: calEvent.id
|
||||
}
|
||||
})
|
||||
.done(function(data){
|
||||
$("div#timesheet-lines").html('');
|
||||
for (line in data.lines) {
|
||||
$("div#timesheet-lines").append(data.lines[line]);
|
||||
}
|
||||
// Change all timeago dates
|
||||
jQuery("abbr.timeago").timeago();
|
||||
$("img#loading").hide();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
eventSources: [
|
||||
{
|
||||
events: function(start, end, callback) {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<br>
|
||||
<a class="btn btn-mini btn-danger btn-remove-participant" href=""
|
||||
title="Remove {{ user.display_name }}" rel="tooltip" data-id="{{ user.id }}"
|
||||
data-url="{{ url_for('project.work.remove_participant', project_id=project.id, participant_id=user.id) }}">
|
||||
data-url="{{ url_for('project.work.remove_participant', active_id=project.id, participant_id=user.id) }}">
|
||||
<i class="icon-minus-sign icon-white"></i> {{ _('Remove') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -57,12 +57,12 @@
|
|||
<strong>{{ invitation.email }}</strong><br>
|
||||
<a class="btn btn-small btn-reinvite-participant"
|
||||
title="Re-Invite {{ invitation.email }}" rel="tooltip" data-id="{{ invitation.id }}"
|
||||
data-url="{{ url_for('project.work.invitation.resend_invite', invitation_id=invitation.id) }}">
|
||||
data-url="{{ url_for('project.work.invitation.resend_invite', active_id=invitation.id) }}">
|
||||
<i class="icon-share-alt icon-white"></i> {{ _('Resend Invite') }}
|
||||
</a>
|
||||
<a class="btn btn-small btn-warning btn-remove-invite"
|
||||
title="Revoke invitation to {{ invitation.email }}" rel="tooltip" data-id="{{ invitation.id }}"
|
||||
data-url="{{ url_for('project.work.invitation.remove_invite', invitation_id=invitation.id) }}">
|
||||
data-url="{{ url_for('project.work.invitation.remove_invite', active_id=invitation.id) }}">
|
||||
<i class="icon-minus-sign icon-white"></i> {{ _('Revoke Invitation') }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
{% extends 'project/project.jinja' %}
|
||||
|
||||
{% from "_helpers.jinja" import status_label, render_pagination %}
|
||||
{% from "project/_helpers.jinja" import render_task_tree with context %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ super() }}
|
||||
<li class="divider">/</li>
|
||||
<li><a href="{{ url_for('project.work.render_task_list', project_id=project.id) }}">Tasks</a></li>
|
||||
<li><a href="{{ url_for('project.work.render_task_list', project_id=project.id, state='opened') }}">Tasks</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
@ -83,37 +84,7 @@
|
|||
</div>
|
||||
|
||||
{% for task in tasks %}
|
||||
<div class="project-info {{ loop.cycle('project-lightgray-bg', '') }}">
|
||||
<div class="row-fluid">
|
||||
{#<div class="span1"><span class="badge badge-info"></span></div>#}
|
||||
<div class="span1">
|
||||
<p>
|
||||
<a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}">#{{ task.id }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="span11">
|
||||
<p><a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}"><strong>{{ task.name }}</strong></a></p>
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<p><span>by <a href="#">{{ task.created_by.name }}</a> <abbr class="timeago" title="{{ task.create_date }}">{{ task.create_date }}</abbr></span></p>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<p>
|
||||
{#<i class="icon-file"></i><span><a href="#">Code Attached</a></span>#}
|
||||
<i class="icon-comment"></i> <span><a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}"> {{ ngettext('%(num)d comment', '%(num)d comments', task.history|length) }}</a></span>
|
||||
<i class="icon-user"></i> <span><a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}"> {{ ngettext('%(num)d participant', '%(num)d participants', task.participants|length) }}</a></span>
|
||||
{% if task.hours %}
|
||||
<i class="icon-time"></i> <span><a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}"> {{ ngettext('%(num)d hour', '%(num)d hours', task.hours) }}</a></span>
|
||||
{% endif %}
|
||||
{% if task.attachments %}
|
||||
<i class="icon-file"></i> <span><a href="{{ url_for('project.work.render_task', project_id=project.id, task_id=task.id) }}"> {{ ngettext('%(num)d file', '%(num)d files', task.attachments|length) }}</a></span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ render_task_tree(task) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script>
|
||||
|
|
|
@ -85,6 +85,12 @@
|
|||
<label for="estimated_hours">Estimated hours</label>
|
||||
<input class="span12" type="text" placeholder="Estimated hours"
|
||||
name="estimated_hours" id="estimated_hours"/>
|
||||
<label for="tag"> Tags: </label>
|
||||
<select data-placeholder="Choose tags to add into this task" class="chzn-select" style="width: 460px;" multiple id="tag" name="tags">
|
||||
{% for tag in project.tags_for_projects %}
|
||||
<option value="{{ tag.id }}">{{ tag.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="constraint_start_time">Task should start and finish by <small>(Optional):</small></label>
|
||||
<div class="input-append date datepicker span4">
|
||||
<input class="datepicker span10" type="text" name="constraint_start_time" readonly="">
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
{% for state, state_name in states %}
|
||||
<div class="span3">
|
||||
{% for task in tasks_by_employee_by_state[employee][state] %}
|
||||
{{ render_task(task, assigned_to_picture=False, show_effort=False, show_hours=False, show_attachments=False, show_watch_toggle=False, show_project_tag=True, show_project=False, show_progress_state=False) }}
|
||||
{{ render_task(task, assigned_to_picture=False, show_project_tag=True) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<div class="span8">
|
||||
<ul id="myTab" class="nav nav-tabs">
|
||||
<li class="active"><a href="#tab-calendar" data-toggle="tab">Calendar</a></li>
|
||||
<li class=""><a href="#tab-timesheet-lines" data-toggle="tab">Timesheet Lines</a></li>
|
||||
<li class=""><a href="#tab-workweek-lines" data-toggle="tab">Weekly Analysis</a></li>
|
||||
<li class=""><a href="#tab-timesheet-lines" data-toggle="tab">Timesheet Lines</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="span1">
|
||||
|
@ -70,12 +70,6 @@
|
|||
})
|
||||
.done(function(data){
|
||||
$("div#workweek-lines").html(data.work_week);
|
||||
$("div#timesheet-lines").html('');
|
||||
for (line in data.lines) {
|
||||
$("div#timesheet-lines").append(data.lines[line]);
|
||||
}
|
||||
// Change all timeago dates
|
||||
jQuery("abbr.timeago").timeago();
|
||||
callback(data.day_totals);
|
||||
$("img#loading").hide();
|
||||
$('#timesheet-tabs').tab();
|
||||
|
@ -91,6 +85,27 @@
|
|||
var load_calendar = function() {
|
||||
$('#calendar').fullCalendar({
|
||||
header: {right: 'today month,basicWeek prev,next'},
|
||||
eventClick: function(calEvent, jsEvent, view) {
|
||||
$("img#loading").show();
|
||||
$("div#timesheet-lines").html('loading...');
|
||||
$('#myTab a:last').tab('show');
|
||||
$.ajax({
|
||||
url: "{{ url_for('project.work.render_timesheet', project_id=project.id) }}",
|
||||
data: {
|
||||
timesheet_lines_of: calEvent.id
|
||||
}
|
||||
})
|
||||
.done(function(data){
|
||||
$("div#timesheet-lines").html('');
|
||||
for (line in data.lines) {
|
||||
$("div#timesheet-lines").append(data.lines[line]);
|
||||
}
|
||||
// Change all timeago dates
|
||||
jQuery("abbr.timeago").timeago();
|
||||
$("img#loading").hide();
|
||||
});
|
||||
|
||||
},
|
||||
eventSources: [
|
||||
{
|
||||
events: function(start, end, callback) {
|
||||
|
|
|
@ -41,6 +41,8 @@ class TestNereidProject(NereidTestCase):
|
|||
this method is called before each test function execution.
|
||||
"""
|
||||
trytond.tests.test_tryton.install_module('nereid_project')
|
||||
self.ActivityAllowedModel = POOL.get('nereid.activity.allowed_model')
|
||||
self.Model = POOL.get('ir.model')
|
||||
self.Project = POOL.get('project.work')
|
||||
self.Company = POOL.get('company.company')
|
||||
self.Employee = POOL.get('company.employee')
|
||||
|
@ -200,11 +202,11 @@ class TestNereidProject(NereidTestCase):
|
|||
'localhost/project/project.jinja': '{{ project.name }}',
|
||||
'localhost/project/home.jinja': '{{ projects|length }}',
|
||||
'localhost/project/timesheet.jinja': '{{ employees|length }}',
|
||||
'localhost/project/project-files.jinja':
|
||||
'localhost/project/files.jinja':
|
||||
'{{ project.children[0].attachments|length }}',
|
||||
'localhost/project/project-permissions.jinja':
|
||||
'localhost/project/permissions.jinja':
|
||||
'{{ invitations|length }}',
|
||||
'localhost/project/project-plan.jinja': '{{ }}',
|
||||
'localhost/project/plan.jinja': '{{ }}',
|
||||
'localhost/project/compare-performance.jinja':
|
||||
'{{ employees|length }}',
|
||||
'localhost/project/emails/text_content.jinja': '',
|
||||
|
@ -768,6 +770,12 @@ class TestNereidProject(NereidTestCase):
|
|||
'parent': project.id,
|
||||
'company': data['company'].id,
|
||||
})
|
||||
task2 = self.Project.create({
|
||||
'name': 'PQR_task',
|
||||
'comment': 'task2',
|
||||
'parent': project.id,
|
||||
'company': data['company'].id,
|
||||
})
|
||||
|
||||
login_data = {
|
||||
'email': 'email@example.com',
|
||||
|
@ -788,16 +796,79 @@ class TestNereidProject(NereidTestCase):
|
|||
response = c.post(
|
||||
'/en_US/attachment/-upload',
|
||||
data={
|
||||
'file': (StringIO('testfile contents'), 'test.txt'),
|
||||
'file': (StringIO('testfile contents'), 'test1.txt'),
|
||||
'task': task1.id,
|
||||
},
|
||||
content_type="multipart/form-data"
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# File is added successfully
|
||||
response = c.get('en_US/login')
|
||||
self.assertTrue(
|
||||
u'Attachment added to ABC_task' in response.data
|
||||
)
|
||||
|
||||
# File 'test1.txt' added successfully in task1
|
||||
self.assertEqual(len(self.Attachment.search([])), 1)
|
||||
|
||||
# Add same file to other task
|
||||
response = c.post(
|
||||
'/en_US/attachment/-upload',
|
||||
data={
|
||||
'file': (StringIO('testfile contents'), 'test1.txt'),
|
||||
'task': task2.id,
|
||||
},
|
||||
content_type="multipart/form-data"
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = c.get('en_US/login')
|
||||
self.assertTrue(
|
||||
u'Attachment added to PQR_task' in response.data
|
||||
)
|
||||
|
||||
# Same file 'test1.txt' added successfully in task2
|
||||
self.assertEqual(len(self.Attachment.search([])), 2)
|
||||
|
||||
# Upload same file again
|
||||
response = c.post(
|
||||
'/en_US/attachment/-upload',
|
||||
data={
|
||||
'file': (StringIO('testfile contents'), 'test1.txt'),
|
||||
'task': task1.id,
|
||||
},
|
||||
content_type="multipart/form-data"
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Check Flash Message
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'File already exists with same name, please choose ' +
|
||||
'another file or rename this file to upload !!' in
|
||||
response.data
|
||||
)
|
||||
|
||||
# No same file added in same task
|
||||
self.assertEqual(len(self.Attachment.search([])), 2)
|
||||
|
||||
# Add same file content with different file name
|
||||
response = c.post(
|
||||
'/en_US/attachment/-upload',
|
||||
data={
|
||||
'file': (StringIO('testfile contents'), 'test2.txt'),
|
||||
'task': task1.id,
|
||||
},
|
||||
content_type="multipart/form-data"
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# 2nd file with same content is added successfully
|
||||
response = c.get('/en_US/project-%d/-files' % project.id)
|
||||
self.assertEqual(response.data, '1')
|
||||
self.assertEqual(response.data, '2')
|
||||
|
||||
# Total file added in attachments
|
||||
self.assertEqual(len(self.Attachment.search([])), 3)
|
||||
|
||||
def test_0130_render_files(self):
|
||||
"""
|
||||
|
|
|
@ -40,6 +40,8 @@ class TestTask(NereidTestCase):
|
|||
this method is called before each test function execution.
|
||||
"""
|
||||
trytond.tests.test_tryton.install_module('nereid_project')
|
||||
self.ActivityAllowedModel = POOL.get('nereid.activity.allowed_model')
|
||||
self.Model = POOL.get('ir.model')
|
||||
self.Company = POOL.get('company.company')
|
||||
self.Employee = POOL.get('company.employee')
|
||||
self.Currency = POOL.get('currency.currency')
|
||||
|
@ -162,6 +164,11 @@ class TestTask(NereidTestCase):
|
|||
'color': 'color2',
|
||||
'project': project1.id
|
||||
})
|
||||
tag3 = self.Tag.create({
|
||||
'name': 'tag3',
|
||||
'color': 'color3',
|
||||
'project': project1.id
|
||||
})
|
||||
|
||||
# Nereid Permission
|
||||
permission = self.Permission.search([
|
||||
|
@ -190,6 +197,7 @@ class TestTask(NereidTestCase):
|
|||
'project1': project1,
|
||||
'tag1': tag1,
|
||||
'tag2': tag2,
|
||||
'tag3': tag3,
|
||||
}
|
||||
|
||||
def create_task_dafaults(self):
|
||||
|
@ -740,51 +748,53 @@ class TestTask(NereidTestCase):
|
|||
# Login Success
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.location, 'http://localhost/en_US/')
|
||||
with Transaction().set_context(
|
||||
{'company': data['company'].id}
|
||||
):
|
||||
# Mark time
|
||||
response = c.post(
|
||||
'/en_US/task-%d/-mark-time' % task.id,
|
||||
data={
|
||||
'hours': '8',
|
||||
}
|
||||
)
|
||||
|
||||
# Mark time
|
||||
response = c.post(
|
||||
'/en_US/task-%d/-mark-time' % task.id,
|
||||
data={
|
||||
'hours': '8',
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Check Flash Message
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'Time has been marked on task ABC_task' in
|
||||
response.data
|
||||
)
|
||||
|
||||
# Check Flash Message
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'Time has been marked on task ABC_task' in
|
||||
response.data
|
||||
)
|
||||
# Logout
|
||||
response = c.get('/en_US/logout')
|
||||
|
||||
# Logout
|
||||
response = c.get('/en_US/logout')
|
||||
# Login with other user
|
||||
response = c.post('/en_US/login', data=login_data2)
|
||||
|
||||
# Login with other user
|
||||
response = c.post('/en_US/login', data=login_data2)
|
||||
# Login Success
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.location, 'http://localhost/en_US/')
|
||||
|
||||
# Login Success
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.location, 'http://localhost/en_US/')
|
||||
# Mark time when user is not employee
|
||||
response = c.post(
|
||||
'/en_US/task-%d/-mark-time' % task.id,
|
||||
data={
|
||||
'hours': '8',
|
||||
}
|
||||
)
|
||||
|
||||
# Mark time when user is not employee
|
||||
response = c.post(
|
||||
'/en_US/task-%d/-mark-time' % task.id,
|
||||
data={
|
||||
'hours': '8',
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = c.get('/en_US/logout')
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
response = c.get('/en_US/logout')
|
||||
|
||||
# Check Flash Message
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'Only employees can mark time on tasks!' in
|
||||
response.data
|
||||
)
|
||||
# Check Flash Message
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'Only employees can mark time on tasks!' in
|
||||
response.data
|
||||
)
|
||||
|
||||
def test_0130_change_estimated_hours(self):
|
||||
"""
|
||||
|
@ -1042,6 +1052,69 @@ class TestTask(NereidTestCase):
|
|||
2
|
||||
)
|
||||
|
||||
def test_0200_create_task_with_multiple_tags(self):
|
||||
"""
|
||||
Adding more than one tag to task which already exist in a project
|
||||
"""
|
||||
with Transaction().start(DB_NAME, USER, CONTEXT):
|
||||
data = self.create_defaults()
|
||||
app = self.get_app(DEBUG=True)
|
||||
|
||||
login_data = {
|
||||
'email': 'email@example.com',
|
||||
'password': 'password',
|
||||
}
|
||||
with app.test_client() as c:
|
||||
response = c.post('/en_US/login', data=login_data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
with Transaction().set_context(
|
||||
{'company': data['company'].id}
|
||||
):
|
||||
# No task created
|
||||
self.assertEqual(
|
||||
len(self.Project.search([('type', '=', 'task')])),
|
||||
0
|
||||
)
|
||||
|
||||
# Create Task
|
||||
response = c.post(
|
||||
'/en_US/project-%d/task/-new' % data['project1'].id,
|
||||
data={
|
||||
'name': 'Task with multiple tags',
|
||||
'description': 'Multi selection tags field',
|
||||
'tags': [
|
||||
data['tag1'].id,
|
||||
data['tag2'].id,
|
||||
data['tag3'].id,
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# One task created
|
||||
self.assertEqual(
|
||||
len(self.Project.search([('type', '=', 'task')])),
|
||||
1
|
||||
)
|
||||
self.assertTrue(
|
||||
self.Project.search([
|
||||
('name', '=', 'Task with multiple tags')
|
||||
])
|
||||
)
|
||||
|
||||
task, = self.Project.search([
|
||||
('name', '=', 'Task with multiple tags'),
|
||||
])
|
||||
|
||||
# Tags added in above created task
|
||||
self.assertEqual(len(task.tags), 3)
|
||||
|
||||
response = c.get('/en_US/login')
|
||||
self.assertTrue(
|
||||
u'Task successfully added to project ABC' in
|
||||
response.data
|
||||
)
|
||||
|
||||
|
||||
def suite():
|
||||
"Nereid test suite"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[tryton]
|
||||
version=2.6.1.1
|
||||
version=2.6.2.0
|
||||
depends:
|
||||
ir
|
||||
res
|
||||
|
@ -10,6 +10,7 @@ depends:
|
|||
project_revenue
|
||||
project_plan
|
||||
nereid
|
||||
nereid_activity_stream
|
||||
xml:
|
||||
urls.xml
|
||||
company.xml
|
||||
|
|