Merge branch 'release/2.6.2.0'

This commit is contained in:
Sharoon Thomas 2013-08-09 13:57:44 +05:30
commit c35a8d49da
60 changed files with 17381 additions and 265 deletions

View File

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

177
doc/Makefile Normal file
View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
doc/source/images/done.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
doc/source/images/task.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
doc/source/images/tasks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
doc/source/images/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

25
doc/source/index.rst Normal file
View File

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

123
doc/source/install.rst Normal file
View File

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

62
doc/source/project.rst Normal file
View File

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

209
doc/source/quickstart.rst Normal file
View File

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

589
doc/source/tutorial.rst Normal file
View File

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

43
fabfile.py vendored Normal file
View File

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

View File

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

View File

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

102
static/css/custom.css Normal file
View File

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

15003
static/js/jquery-ui.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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