Merge branch 'master' of gitlab.com:flectra-hq/flectra
6
.gitignore
vendored
@ -15,10 +15,10 @@ __pycache__/
|
|||||||
# hg stuff
|
# hg stuff
|
||||||
*.orig
|
*.orig
|
||||||
status
|
status
|
||||||
# odoo filestore
|
# flectra filestore
|
||||||
odoo/filestore
|
flectra/filestore
|
||||||
# maintenance migration scripts
|
# maintenance migration scripts
|
||||||
odoo/addons/base/maintenance
|
flectra/addons/base/maintenance
|
||||||
|
|
||||||
# generated for windows installer?
|
# generated for windows installer?
|
||||||
install/win32/*.bat
|
install/win32/*.bat
|
||||||
|
46
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at flectra@flectrahq.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[homepage]: http://contributor-covenant.org
|
||||||
|
[version]: http://contributor-covenant.org/version/1/4/
|
@ -1,6 +1,8 @@
|
|||||||
Contributing to Odoo
|
Contributing to Flectra
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
#TODO
|
||||||
|
|
||||||
[Full contribution guidelines](https://github.com/odoo/odoo/wiki/Contributing)
|
[Full contribution guidelines](https://github.com/odoo/odoo/wiki/Contributing)
|
||||||
|
|
||||||
TL;DR
|
TL;DR
|
||||||
|
38
README.md
@ -1,33 +1,17 @@
|
|||||||
[![Build Status](http://runbot.odoo.com/runbot/badge/flat/1/11.0.svg)](http://runbot.odoo.com/runbot)
|
|
||||||
[![Tech Doc](http://img.shields.io/badge/11.0-docs-875A7B.svg?style=flat&colorA=8F8F8F)](http://www.odoo.com/documentation/11.0)
|
|
||||||
[![Help](http://img.shields.io/badge/11.0-help-875A7B.svg?style=flat&colorA=8F8F8F)](https://www.odoo.com/forum/help-1)
|
|
||||||
[![Nightly Builds](http://img.shields.io/badge/11.0-nightly-875A7B.svg?style=flat&colorA=8F8F8F)](http://nightly.odoo.com/)
|
|
||||||
|
|
||||||
Odoo
|
Flectra
|
||||||
----
|
=======
|
||||||
|
|
||||||
Odoo is a suite of web based open source business apps.
|
Flectra is a suite of web based open source business apps forked from Odoo.
|
||||||
|
|
||||||
The main Odoo Apps include an <a href="https://www.odoo.com/page/crm">Open Source CRM</a>,
|
The main Flectra Apps include an Open Source CRM,Website Builder,eCommerce,Warehouse Management,
|
||||||
<a href="https://www.odoo.com/page/website-builder">Website Builder</a>,
|
Project Management,Billing & Accounting,Point of Sale,Human Resources,Marketing,Manufacturing,
|
||||||
<a href="https://www.odoo.com/page/e-commerce">eCommerce</a>,
|
Purchase Management and many more.
|
||||||
<a href="https://www.odoo.com/page/warehouse">Warehouse Management</a>,
|
|
||||||
<a href="https://www.odoo.com/page/project-management">Project Management</a>,
|
|
||||||
<a href="https://www.odoo.com/page/accounting">Billing & Accounting</a>,
|
|
||||||
<a href="https://www.odoo.com/page/point-of-sale">Point of Sale</a>,
|
|
||||||
<a href="https://www.odoo.com/page/employees">Human Resources</a>,
|
|
||||||
<a href="https://www.odoo.com/page/lead-automation">Marketing</a>,
|
|
||||||
<a href="https://www.odoo.com/page/manufacturing">Manufacturing</a>,
|
|
||||||
<a href="https://www.odoo.com/page/purchase">Purchase Management</a>,
|
|
||||||
<a href="https://www.odoo.com/#apps">...</a>
|
|
||||||
|
|
||||||
Odoo Apps can be used as stand-alone applications, but they also integrate seamlessly so you get
|
Flectra Apps can be used as stand-alone applications, but they also integrate seamlessly so you get
|
||||||
a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you install several Apps.
|
a full-featured <a href="https://flectrahq.com">Open Source ERP</a> when you install several Apps.
|
||||||
|
|
||||||
|
|
||||||
Getting started with Odoo
|
Getting started with Flectra
|
||||||
-------------------------
|
----------------------------
|
||||||
For a standard installation please follow the <a href="https://www.odoo.com/documentation/11.0/setup/install.html">Setup instructions</a>
|
For a standard installation please follow this <a href="https://gist.github.com/flectrahqadmin/d4c827577b3251a9505d3b697b1068be">gist</a>.
|
||||||
from the documentation.
|
|
||||||
|
|
||||||
Then follow <a href="https://www.odoo.com/documentation/11.0/tutorials.html">the developer tutorials</a>
|
|
||||||
|
@ -183,6 +183,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<t groups="account.group_account_user">
|
||||||
|
<h2>Fiscal Periods</h2>
|
||||||
|
<div class="row mt16 o_settings_container" id="accounting_reports">
|
||||||
|
<div class="col-xs-12 col-md-6 o_setting_box" id="fiscalyear" invisible="1"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
<h2>Customer Payments</h2>
|
<h2>Customer Payments</h2>
|
||||||
<div class="row mt16 o_settings_container">
|
<div class="row mt16 o_settings_container">
|
||||||
<div class="col-xs-12 col-md-6 o_setting_box" id="account_followup" title="This feature is useful if you issue a high amounts of invoices.">
|
<div class="col-xs-12 col-md-6 o_setting_box" id="account_followup" title="This feature is useful if you issue a high amounts of invoices.">
|
||||||
|
@ -30,10 +30,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p>Enjoy your Flectra experience,</p>
|
<p>Enjoy your Flectra experience,</p>
|
||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
|
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.8 KiB |
@ -2,6 +2,7 @@
|
|||||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from flectra import api, fields, models, _
|
from flectra import api, fields, models, _
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class ResConfigSettings(models.TransientModel):
|
class ResConfigSettings(models.TransientModel):
|
||||||
@ -35,17 +36,26 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
help="Allows to work in a multi currency environment")
|
help="Allows to work in a multi currency environment")
|
||||||
paperformat_id = fields.Many2one(related="company_id.paperformat_id", string='Paper format')
|
paperformat_id = fields.Many2one(related="company_id.paperformat_id", string='Paper format')
|
||||||
external_report_layout = fields.Selection(related="company_id.external_report_layout")
|
external_report_layout = fields.Selection(related="company_id.external_report_layout")
|
||||||
|
send_statistics = fields.Boolean(
|
||||||
|
"Send Statistics")
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_values(self):
|
def get_values(self):
|
||||||
res = super(ResConfigSettings, self).get_values()
|
res = super(ResConfigSettings, self).get_values()
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
default_external_email_server = params.get_param('base_setup.default_external_email_server', default=False)
|
default_external_email_server = params.get_param('base_setup.default_external_email_server', default=False)
|
||||||
|
send_statistics = params._get_param(
|
||||||
|
'base_setup.send_statistics')
|
||||||
|
if send_statistics is None:
|
||||||
|
send_statistics = 'true'
|
||||||
|
if send_statistics in ['true', 'false']:
|
||||||
|
send_statistics = json.loads(send_statistics)
|
||||||
default_user_rights = params.get_param('base_setup.default_user_rights', default=False)
|
default_user_rights = params.get_param('base_setup.default_user_rights', default=False)
|
||||||
default_custom_report_footer = params.get_param('base_setup.default_custom_report_footer', default=False)
|
default_custom_report_footer = params.get_param('base_setup.default_custom_report_footer', default=False)
|
||||||
res.update(
|
res.update(
|
||||||
default_external_email_server=default_external_email_server,
|
default_external_email_server=default_external_email_server,
|
||||||
default_user_rights=default_user_rights,
|
default_user_rights=default_user_rights,
|
||||||
|
send_statistics=send_statistics,
|
||||||
default_custom_report_footer=default_custom_report_footer,
|
default_custom_report_footer=default_custom_report_footer,
|
||||||
company_share_partner=not self.env.ref('base.res_partner_rule').active,
|
company_share_partner=not self.env.ref('base.res_partner_rule').active,
|
||||||
)
|
)
|
||||||
@ -57,6 +67,11 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_external_email_server", self.default_external_email_server)
|
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_external_email_server", self.default_external_email_server)
|
||||||
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_user_rights", self.default_user_rights)
|
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_user_rights", self.default_user_rights)
|
||||||
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_custom_report_footer", self.default_custom_report_footer)
|
self.env['ir.config_parameter'].sudo().set_param("base_setup.default_custom_report_footer", self.default_custom_report_footer)
|
||||||
|
send_statistics = 'true'
|
||||||
|
if not self.send_statistics:
|
||||||
|
send_statistics = 'false'
|
||||||
|
self.env['ir.config_parameter'].sudo().set_param(
|
||||||
|
"base_setup.send_statistics", send_statistics)
|
||||||
self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner})
|
self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner})
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
@ -218,6 +218,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h2>System Parameter</h2>
|
||||||
|
<div class="row mt16 o_settings_container" id="send_statistics">
|
||||||
|
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane">
|
||||||
|
<field name="send_statistics" />
|
||||||
|
</div>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="send_statistics"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Enable/disable sending statistics to Flectra's Warranty Server.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 5.0 KiB |
@ -9,9 +9,9 @@ from flectra.tools import pycompat
|
|||||||
|
|
||||||
class BusController(Controller):
|
class BusController(Controller):
|
||||||
""" Examples:
|
""" Examples:
|
||||||
openerp.jsonRpc('/longpolling/poll','call',{"channels":["c1"],last:0}).then(function(r){console.log(r)});
|
flectra.jsonRpc('/longpolling/poll','call',{"channels":["c1"],last:0}).then(function(r){console.log(r)});
|
||||||
openerp.jsonRpc('/longpolling/send','call',{"channel":"c1","message":"m1"});
|
flectra.jsonRpc('/longpolling/send','call',{"channel":"c1","message":"m1"});
|
||||||
openerp.jsonRpc('/longpolling/send','call',{"channel":"c2","message":"m2"});
|
flectra.jsonRpc('/longpolling/send','call',{"channel":"c2","message":"m2"});
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@route('/longpolling/send', type="json", auth="public")
|
@route('/longpolling/send', type="json", auth="public")
|
||||||
|
@ -121,7 +121,7 @@ class ImDispatch(object):
|
|||||||
current._Thread__daemonic = True # PY2
|
current._Thread__daemonic = True # PY2
|
||||||
current._daemonic = True # PY3
|
current._daemonic = True # PY3
|
||||||
# rename the thread to avoid tests waiting for a longpolling
|
# rename the thread to avoid tests waiting for a longpolling
|
||||||
current.setName("openerp.longpolling.request.%s" % current.ident)
|
current.setName("flectra.longpolling.request.%s" % current.ident)
|
||||||
|
|
||||||
registry = flectra.registry(dbname)
|
registry = flectra.registry(dbname)
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 1.2 KiB |
@ -28,10 +28,8 @@
|
|||||||
Have fun deploying your sales strategy,
|
Have fun deploying your sales strategy,
|
||||||
</p>
|
</p>
|
||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
|
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 11 KiB |
@ -7,7 +7,7 @@
|
|||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Extra Tools',
|
'category': 'Extra Tools',
|
||||||
'description': """
|
'description': """
|
||||||
The module adds the possibility to display data from Odoo in Google Spreadsheets in real time.
|
The module adds the possibility to display data from Flectra in Google Spreadsheets in real time.
|
||||||
=================================================================================================
|
=================================================================================================
|
||||||
""",
|
""",
|
||||||
'depends': ['google_drive'],
|
'depends': ['google_drive'],
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 7.8 KiB |
@ -29,10 +29,8 @@
|
|||||||
<p>Enjoy your Flectra experience,
|
<p>Enjoy your Flectra experience,
|
||||||
</p>
|
</p>
|
||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
|
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
@ -4,7 +4,7 @@
|
|||||||
from flectra import http
|
from flectra import http
|
||||||
from flectra.tools import config
|
from flectra.tools import config
|
||||||
from flectra.addons.web.controllers import main as web
|
from flectra.addons.web.controllers import main as web
|
||||||
from openerp.addons.hw_posbox_homepage.controllers import main as homepage
|
from flectra.addons.hw_posbox_homepage.controllers import main as homepage
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import netifaces as ni
|
import netifaces as ni
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.3 KiB |
@ -1,7 +1,7 @@
|
|||||||
from openerp.modules.registry import RegistryManager
|
from flectra.modules.registry import RegistryManager
|
||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
registry = RegistryManager.get(cr.dbname)
|
registry = RegistryManager.get(cr.dbname)
|
||||||
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
|
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
|
||||||
migrate_tags_on_taxes(cr, registry)
|
migrate_tags_on_taxes(cr, registry)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from openerp.modules.registry import RegistryManager
|
from flectra.modules.registry import RegistryManager
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
registry = RegistryManager.get(cr.dbname)
|
registry = RegistryManager.get(cr.dbname)
|
||||||
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
|
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
|
||||||
migrate_tags_on_taxes(cr, registry)
|
migrate_tags_on_taxes(cr, registry)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from openerp.modules.registry import RegistryManager
|
from flectra.modules.registry import RegistryManager
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
registry = RegistryManager.get(cr.dbname)
|
registry = RegistryManager.get(cr.dbname)
|
||||||
from openerp.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
|
from flectra.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
|
||||||
migrate_set_tags_and_taxes_updatable(cr, registry, 'l10n_fr')
|
migrate_set_tags_and_taxes_updatable(cr, registry, 'l10n_fr')
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from openerp.modules.registry import RegistryManager
|
from flectra.modules.registry import RegistryManager
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
registry = RegistryManager.get(cr.dbname)
|
registry = RegistryManager.get(cr.dbname)
|
||||||
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
|
from flectra.addons.account.models.chart_template import migrate_tags_on_taxes
|
||||||
migrate_tags_on_taxes(cr, registry)
|
migrate_tags_on_taxes(cr, registry)
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 12 KiB |
@ -36,6 +36,7 @@
|
|||||||
'views/ir_actions_views.xml',
|
'views/ir_actions_views.xml',
|
||||||
'views/ir_model_views.xml',
|
'views/ir_model_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
|
'wizard/mass_mail.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
'data/mail_demo.xml',
|
'data/mail_demo.xml',
|
||||||
|
@ -30,35 +30,41 @@ class PublisherWarrantyContract(AbstractModel):
|
|||||||
db_create_date = IrParamSudo.get_param('database.create_date')
|
db_create_date = IrParamSudo.get_param('database.create_date')
|
||||||
limit_date = datetime.datetime.now()
|
limit_date = datetime.datetime.now()
|
||||||
limit_date = limit_date - datetime.timedelta(15)
|
limit_date = limit_date - datetime.timedelta(15)
|
||||||
limit_date_str = limit_date.strftime(misc.DEFAULT_SERVER_DATETIME_FORMAT)
|
limit_date_str = limit_date.strftime(
|
||||||
|
misc.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
nbr_users = Users.search_count([('active', '=', True)])
|
nbr_users = Users.search_count([('active', '=', True)])
|
||||||
nbr_active_users = Users.search_count([("login_date", ">=", limit_date_str), ('active', '=', True)])
|
nbr_active_users = Users.search_count(
|
||||||
|
[("login_date", ">=", limit_date_str), ('active', '=', True)])
|
||||||
nbr_share_users = 0
|
nbr_share_users = 0
|
||||||
nbr_active_share_users = 0
|
nbr_active_share_users = 0
|
||||||
if "share" in Users._fields:
|
if "share" in Users._fields:
|
||||||
nbr_share_users = Users.search_count([("share", "=", True), ('active', '=', True)])
|
nbr_share_users = Users.search_count(
|
||||||
nbr_active_share_users = Users.search_count([("share", "=", True), ("login_date", ">=", limit_date_str), ('active', '=', True)])
|
[("share", "=", True), ('active', '=', True)])
|
||||||
|
nbr_active_share_users = Users.search_count(
|
||||||
|
[("share", "=", True), ("login_date", ">=", limit_date_str),
|
||||||
|
('active', '=', True)])
|
||||||
user = self.env.user
|
user = self.env.user
|
||||||
domain = [('application', '=', True), ('state', 'in', ['installed', 'to upgrade', 'to remove'])]
|
domain = [('application', '=', True),
|
||||||
|
('state', 'in', ['installed', 'to upgrade', 'to remove'])]
|
||||||
apps = self.env['ir.module.module'].sudo().search_read(domain, ['name'])
|
apps = self.env['ir.module.module'].sudo().search_read(domain, ['name'])
|
||||||
|
demo_domain = [('name', 'ilike', 'base'), ('demo', '=', True)]
|
||||||
enterprise_code = IrParamSudo.get_param('database.enterprise_code')
|
demo_data_ids = self.env['ir.module.module'].sudo().search(demo_domain)
|
||||||
|
demo_data = True
|
||||||
|
if not demo_data_ids:
|
||||||
|
demo_data = False
|
||||||
|
support_code = IrParamSudo.get_param('database.support_code')
|
||||||
|
|
||||||
web_base_url = IrParamSudo.get_param('web.base.url')
|
web_base_url = IrParamSudo.get_param('web.base.url')
|
||||||
msg = {
|
msg = {"dbuuid": dbuuid, "nbr_users": nbr_users,
|
||||||
"dbuuid": dbuuid,
|
|
||||||
"nbr_users": nbr_users,
|
|
||||||
"nbr_active_users": nbr_active_users,
|
"nbr_active_users": nbr_active_users,
|
||||||
"nbr_share_users": nbr_share_users,
|
"nbr_share_users": nbr_share_users,
|
||||||
"nbr_active_share_users": nbr_active_share_users,
|
"nbr_active_share_users": nbr_active_share_users,
|
||||||
"dbname": self._cr.dbname,
|
"dbname": self._cr.dbname, "db_create_date": db_create_date,
|
||||||
"db_create_date": db_create_date,
|
"version": release.version, "language": user.lang,
|
||||||
"version": release.version,
|
|
||||||
"language": user.lang,
|
|
||||||
"web_base_url": web_base_url,
|
"web_base_url": web_base_url,
|
||||||
"apps": [app['name'] for app in apps],
|
"apps": [app['name'] for app in apps],
|
||||||
"enterprise_code": enterprise_code,
|
"support_code": support_code,
|
||||||
}
|
"demo_data": demo_data}
|
||||||
if user.partner_id.company_id:
|
if user.partner_id.company_id:
|
||||||
company_id = user.partner_id.company_id
|
company_id = user.partner_id.company_id
|
||||||
msg.update(company_id.read(["name", "email", "phone"])[0])
|
msg.update(company_id.read(["name", "email", "phone"])[0])
|
||||||
@ -88,13 +94,20 @@ class PublisherWarrantyContract(AbstractModel):
|
|||||||
@type cron_mode: boolean
|
@type cron_mode: boolean
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Code will be execute only if parameter value 'True'
|
||||||
|
parameter_id = self.env['ir.config_parameter'].sudo().get_param(
|
||||||
|
'base_setup.send_statistics')
|
||||||
|
if parameter_id != 'true':
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
result = self._get_sys_logs()
|
result = self._get_sys_logs()
|
||||||
except Exception:
|
except Exception:
|
||||||
if cron_mode: # we don't want to see any stack trace in cron
|
if cron_mode: # we don't want to see any stack trace in cron
|
||||||
return False
|
return False
|
||||||
_logger.debug("Exception while sending a get logs messages", exc_info=1)
|
_logger.debug("Exception while sending a get logs messages",
|
||||||
raise UserError(_("Error during communication with the publisher warranty server."))
|
exc_info=1)
|
||||||
|
raise UserError(_(
|
||||||
|
"Error during communication with the publisher warranty server."))
|
||||||
# old behavior based on res.log; now on mail.message, that is not necessarily installed
|
# old behavior based on res.log; now on mail.message, that is not necessarily installed
|
||||||
user = self.env['res.users'].sudo().browse(SUPERUSER_ID)
|
user = self.env['res.users'].sudo().browse(SUPERUSER_ID)
|
||||||
poster = self.sudo().env.ref('mail.channel_all_employees')
|
poster = self.sudo().env.ref('mail.channel_all_employees')
|
||||||
@ -104,15 +117,20 @@ class PublisherWarrantyContract(AbstractModel):
|
|||||||
poster = user
|
poster = user
|
||||||
for message in result["messages"]:
|
for message in result["messages"]:
|
||||||
try:
|
try:
|
||||||
poster.message_post(body=message, subtype='mt_comment', partner_ids=[user.partner_id.id])
|
poster.message_post(body=message, subtype='mt_comment',
|
||||||
|
partner_ids=[user.partner_id.id])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if result.get('enterprise_info'):
|
if result.get('support_info'):
|
||||||
# Update expiration date
|
# Update expiration date
|
||||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||||
set_param('database.expiration_date', result['enterprise_info'].get('expiration_date'))
|
set_param('database.expiration_date',
|
||||||
set_param('database.expiration_reason', result['enterprise_info'].get('expiration_reason', 'trial'))
|
result['support_info'].get('expiration_date'))
|
||||||
set_param('database.enterprise_code', result['enterprise_info'].get('enterprise_code'))
|
set_param('database.expiration_reason',
|
||||||
|
result['support_info'].get('expiration_reason',
|
||||||
|
'trial'))
|
||||||
|
set_param('database.support_code',
|
||||||
|
result['support_info'].get('support_code'))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
if cron_mode:
|
if cron_mode:
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<templates id="template" xml:space="preserve">
|
|
||||||
<t t-name="WebClient.announcement_bar">
|
|
||||||
<div class="openerp" id="announcement_bar_table">
|
|
||||||
<table class="oe_webclient">
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="announcement_bar">
|
|
||||||
<span class="message"></span>
|
|
||||||
<span class="url">
|
|
||||||
<a href="https://services.openerp.com/openerp-enterprise/ab/register" target="_blank"></a>
|
|
||||||
</span>
|
|
||||||
<span class="close"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
@ -5,3 +5,5 @@ from . import invite
|
|||||||
from . import mail_compose_message
|
from . import mail_compose_message
|
||||||
from . import email_template_preview
|
from . import email_template_preview
|
||||||
from . import base_module_uninstall
|
from . import base_module_uninstall
|
||||||
|
from . import mass_mail
|
||||||
|
|
||||||
|
23
addons/mail/wizard/mass_mail.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from flectra import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MassMail(models.TransientModel):
|
||||||
|
_name = 'mass.mail'
|
||||||
|
_description = 'Mass Mail Options'
|
||||||
|
|
||||||
|
message = fields.Char('Message')
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def mass_emails(self):
|
||||||
|
send_method = self.env.context['mail']
|
||||||
|
active_ids = self.env.context['active_ids']
|
||||||
|
lines = self.env['mail.mail'].search([( 'id', 'in', active_ids)])
|
||||||
|
if send_method == 'mark_outgoing':
|
||||||
|
lines.mark_outgoing()
|
||||||
|
elif send_method == 'send':
|
||||||
|
lines.send()
|
||||||
|
elif send_method == 'cancel':
|
||||||
|
lines.cancel()
|
71
addons/mail/wizard/mass_mail.xml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<flectra>
|
||||||
|
<data>
|
||||||
|
<record id="view_mass_retry_mail_form" model="ir.ui.view">
|
||||||
|
<field name="name">mass.retry.mail.form</field>
|
||||||
|
<field name="model">mass.mail</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Retry">
|
||||||
|
<h2>Are you sure?</h2>
|
||||||
|
<footer>
|
||||||
|
<span groups="base.group_user">
|
||||||
|
<button string="Yes" type="object"
|
||||||
|
name="mass_emails" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button special="cancel" string="No" type="object" />
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<act_window id="action_mass_retry_mail_view" multi="True"
|
||||||
|
context="{'mail': 'mark_outgoing'}"
|
||||||
|
name="Retry" res_model="mass.mail" src_model="mail.mail"
|
||||||
|
view_mode="form" target="new" view_type="form" />
|
||||||
|
|
||||||
|
<record id="view_mass_resend_mail_form" model="ir.ui.view">
|
||||||
|
<field name="name">mass.resend.mail.form</field>
|
||||||
|
<field name="model">mass.mail</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Resend">
|
||||||
|
<h2>Are you sure?</h2>
|
||||||
|
<footer>
|
||||||
|
<span groups="base.group_user">
|
||||||
|
<button string="Yes" type="object"
|
||||||
|
name="mass_emails" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button special="cancel" string="No" type="object" />
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<act_window id="action_mass_resend_mail_view" multi="True"
|
||||||
|
context="{'mail': 'send'}"
|
||||||
|
name="Resend" res_model="mass.mail" src_model="mail.mail"
|
||||||
|
view_mode="form" target="new" view_type="form" />
|
||||||
|
|
||||||
|
<record id="view_mass_cancel_mail_form" model="ir.ui.view">
|
||||||
|
<field name="name">mass.cancel.mail.form</field>
|
||||||
|
<field name="model">mass.mail</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Cancel">
|
||||||
|
<h2>Are you sure?</h2>
|
||||||
|
<footer>
|
||||||
|
<span groups="base.group_user">
|
||||||
|
<button string="Yes" type="object"
|
||||||
|
name="mass_emails" class="oe_highlight"/>
|
||||||
|
or
|
||||||
|
<button special="cancel" string="No" type="object" />
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<act_window id="action_mass_cancel_mail_view" multi="True"
|
||||||
|
context="{'mail': 'cancel'}"
|
||||||
|
name="Cancel" res_model="mass.mail" src_model="mail.mail"
|
||||||
|
view_mode="form" target="new" view_type="form" />
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</flectra>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
63
addons/password_security/README.rst
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
|
||||||
|
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||||
|
:alt: License: LGPL-3
|
||||||
|
|
||||||
|
=================
|
||||||
|
Password Security
|
||||||
|
=================
|
||||||
|
|
||||||
|
This module allows admin to set company-level password security requirements
|
||||||
|
and enforces them on the user.
|
||||||
|
|
||||||
|
It contains features such as
|
||||||
|
|
||||||
|
* Password expiration days
|
||||||
|
* Password length requirement
|
||||||
|
* Password minimum number of lowercase letters
|
||||||
|
* Password minimum number of uppercase letters
|
||||||
|
* Password minimum number of numbers
|
||||||
|
* Password minimum number of special characters
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
# Navigate to company you would like to set requirements on
|
||||||
|
# Click the ``Password Policy`` page
|
||||||
|
# Set the policies to your liking.
|
||||||
|
|
||||||
|
Password complexity requirements will be enforced upon next password change for
|
||||||
|
any user in that company.
|
||||||
|
|
||||||
|
|
||||||
|
Settings & Defaults
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
These are defined at the company level:
|
||||||
|
|
||||||
|
===================== ======= ===================================================
|
||||||
|
Name Default Description
|
||||||
|
===================== ======= ===================================================
|
||||||
|
password_expiration 60 Days until passwords expire
|
||||||
|
password_length 12 Minimum number of characters in password
|
||||||
|
password_lower 0 Minimum number of lowercase letter in password
|
||||||
|
password_upper 0 Minimum number of uppercase letters in password
|
||||||
|
password_numeric 0 Minimum number of number in password
|
||||||
|
password_special 0 Minimum number of unique special character in password
|
||||||
|
password_history 30 Disallow reuse of this many previous passwords
|
||||||
|
password_minimum 24 Amount of hours that must pass until another reset
|
||||||
|
===================== ======= ===================================================
|
||||||
|
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* James Foster <jfoster@laslabs.com>
|
||||||
|
* Dave Lasley <dave@laslabs.com>
|
5
addons/password_security/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
25
addons/password_security/__manifest__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
{
|
||||||
|
|
||||||
|
'name': 'Password Security',
|
||||||
|
"summary": "Allow admin to set password security requirements.",
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "LasLabs, Odoo Community Association (OCA), FlectraHQ",
|
||||||
|
'category': 'Base',
|
||||||
|
'depends': [
|
||||||
|
'auth_crypt',
|
||||||
|
'auth_signup',
|
||||||
|
],
|
||||||
|
"website": "https://laslabs.com",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"data": [
|
||||||
|
'views/res_company_view.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'security/res_users_pass_history.xml',
|
||||||
|
],
|
||||||
|
"demo": [
|
||||||
|
'demo/res_users.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
}
|
4
addons/password_security/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from . import main
|
93
addons/password_security/controllers/main.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from flectra import http
|
||||||
|
from flectra.http import request
|
||||||
|
from flectra.addons.auth_signup.controllers.main import AuthSignupHome
|
||||||
|
from flectra.addons.web.controllers.main import ensure_db, Session
|
||||||
|
|
||||||
|
from ..exceptions import PassError
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordSecuritySession(Session):
|
||||||
|
|
||||||
|
@http.route()
|
||||||
|
def change_password(self, fields):
|
||||||
|
new_password = operator.itemgetter('new_password')(
|
||||||
|
dict(map(operator.itemgetter('name', 'value'), fields))
|
||||||
|
)
|
||||||
|
user_id = request.env.user
|
||||||
|
user_id._check_password(new_password)
|
||||||
|
return super(PasswordSecuritySession, self).change_password(fields)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordSecurityHome(AuthSignupHome):
|
||||||
|
|
||||||
|
def do_signup(self, qcontext):
|
||||||
|
password = qcontext.get('password')
|
||||||
|
user_id = request.env.user
|
||||||
|
user_id._check_password(password)
|
||||||
|
return super(PasswordSecurityHome, self).do_signup(qcontext)
|
||||||
|
|
||||||
|
@http.route()
|
||||||
|
def web_login(self, *args, **kw):
|
||||||
|
ensure_db()
|
||||||
|
response = super(PasswordSecurityHome, self).web_login(*args, **kw)
|
||||||
|
if not request.httprequest.method == 'POST':
|
||||||
|
return response
|
||||||
|
uid = request.session.authenticate(
|
||||||
|
request.session.db,
|
||||||
|
request.params['login'],
|
||||||
|
request.params['password']
|
||||||
|
)
|
||||||
|
if not uid:
|
||||||
|
return response
|
||||||
|
users_obj = request.env['res.users'].sudo()
|
||||||
|
user_id = users_obj.browse(request.uid)
|
||||||
|
if not user_id._password_has_expired():
|
||||||
|
return response
|
||||||
|
user_id.action_expire_password()
|
||||||
|
request.session.logout(keep_db=True)
|
||||||
|
redirect = user_id.partner_id.signup_url
|
||||||
|
return http.redirect_with_hash(redirect)
|
||||||
|
|
||||||
|
@http.route()
|
||||||
|
def web_auth_signup(self, *args, **kw):
|
||||||
|
try:
|
||||||
|
return super(PasswordSecurityHome, self).web_auth_signup(
|
||||||
|
*args, **kw
|
||||||
|
)
|
||||||
|
except PassError as e:
|
||||||
|
qcontext = self.get_auth_signup_qcontext()
|
||||||
|
qcontext['error'] = e.message
|
||||||
|
return request.render('auth_signup.signup', qcontext)
|
||||||
|
|
||||||
|
@http.route()
|
||||||
|
def web_auth_reset_password(self, *args, **kw):
|
||||||
|
""" It provides hook to disallow front-facing resets inside of min
|
||||||
|
Unfortuantely had to reimplement some core logic here because of
|
||||||
|
nested logic in parent
|
||||||
|
"""
|
||||||
|
qcontext = self.get_auth_signup_qcontext()
|
||||||
|
if (
|
||||||
|
request.httprequest.method == 'POST' and
|
||||||
|
qcontext.get('login') and
|
||||||
|
'error' not in qcontext and
|
||||||
|
'token' not in qcontext
|
||||||
|
):
|
||||||
|
login = qcontext.get('login')
|
||||||
|
user_ids = request.env.sudo().search(
|
||||||
|
[('login', '=', login)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
if not user_ids:
|
||||||
|
user_ids = request.env.sudo().search(
|
||||||
|
[('email', '=', login)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
user_ids._validate_pass_reset()
|
||||||
|
return super(PasswordSecurityHome, self).web_auth_reset_password(
|
||||||
|
*args, **kw
|
||||||
|
)
|
16
addons/password_security/demo/res_users.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2016 LasLabs Inc.
|
||||||
|
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<flectra>
|
||||||
|
|
||||||
|
<record id="base.user_root" model="res.users">
|
||||||
|
<field name="password_write_date"
|
||||||
|
eval="datetime.now()"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</flectra>
|
12
addons/password_security/exceptions.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from flectra.exceptions import Warning as UserError
|
||||||
|
|
||||||
|
|
||||||
|
class PassError(UserError):
|
||||||
|
""" Example: When you try to create an insecure password."""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.message = msg
|
||||||
|
super(PassError, self).__init__(msg)
|
6
addons/password_security/models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from . import res_users
|
||||||
|
from . import res_company
|
||||||
|
from . import res_users_pass_history
|
47
addons/password_security/models/res_company.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# Copyright 2004-TODAY FlectraHQ.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from flectra import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
password_expiration = fields.Integer(
|
||||||
|
'Days',
|
||||||
|
default=60,
|
||||||
|
help='How many days until passwords expire',
|
||||||
|
)
|
||||||
|
password_length = fields.Integer(
|
||||||
|
'Characters',
|
||||||
|
default=12,
|
||||||
|
help='Minimum number of characters',
|
||||||
|
)
|
||||||
|
password_lower = fields.Integer(
|
||||||
|
'Lowercase',
|
||||||
|
help='Require lowercase letters',
|
||||||
|
)
|
||||||
|
password_upper = fields.Integer(
|
||||||
|
'Uppercase',
|
||||||
|
help='Require uppercase letters',
|
||||||
|
)
|
||||||
|
password_numeric = fields.Integer(
|
||||||
|
'Numeric',
|
||||||
|
help='Require numeric digits',
|
||||||
|
)
|
||||||
|
password_special = fields.Integer(
|
||||||
|
'Special',
|
||||||
|
help='Require unique special characters',
|
||||||
|
)
|
||||||
|
password_history = fields.Integer(
|
||||||
|
'History',
|
||||||
|
default=30,
|
||||||
|
help='Disallow reuse of this many previous passwords - use negative '
|
||||||
|
'number for infinite, or 0 to disable',
|
||||||
|
)
|
||||||
|
password_minimum = fields.Integer(
|
||||||
|
'Minimum Hours',
|
||||||
|
default=24,
|
||||||
|
help='Amount of hours until a user may change password again',
|
||||||
|
)
|
162
addons/password_security/models/res_users.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# Copyright 2004-TODAY FlectraHQ.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from flectra import api, fields, models, _
|
||||||
|
|
||||||
|
from ..exceptions import PassError
|
||||||
|
|
||||||
|
|
||||||
|
def delta_now(**kwargs):
|
||||||
|
dt = datetime.now() + timedelta(**kwargs)
|
||||||
|
return fields.Datetime.to_string(dt)
|
||||||
|
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
password_write_date = fields.Datetime(
|
||||||
|
'Last password update',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
password_history_ids = fields.One2many(
|
||||||
|
string='Password History',
|
||||||
|
comodel_name='res.users.pass.history',
|
||||||
|
inverse_name='user_id',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
vals['password_write_date'] = fields.Datetime.now()
|
||||||
|
return super(ResUsers, self).create(vals)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def write(self, vals):
|
||||||
|
if vals.get('password'):
|
||||||
|
self._check_password(vals['password'])
|
||||||
|
vals['password_write_date'] = fields.Datetime.now()
|
||||||
|
return super(ResUsers, self).write(vals)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _check_password(self, password):
|
||||||
|
self._check_password_rules(password)
|
||||||
|
self._check_password_history(password)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _check_password_rules(self, password):
|
||||||
|
self.ensure_one()
|
||||||
|
if not password:
|
||||||
|
return True
|
||||||
|
company_id = self.company_id
|
||||||
|
message = []
|
||||||
|
if company_id.password_lower and sum(map(str.islower, password)) < \
|
||||||
|
company_id.password_lower:
|
||||||
|
message.append('\n ' + _('Lowercase letter (At least ' +
|
||||||
|
str(company_id.password_lower) +
|
||||||
|
' character)')
|
||||||
|
)
|
||||||
|
if company_id.password_upper and sum(map(str.isupper, password)) < \
|
||||||
|
company_id.password_upper:
|
||||||
|
message.append('\n ' + _('Uppercase letter (At least ' +
|
||||||
|
str(company_id.password_upper) +
|
||||||
|
' character)')
|
||||||
|
)
|
||||||
|
if company_id.password_numeric and sum(map(str.isdigit, password)) < \
|
||||||
|
company_id.password_numeric:
|
||||||
|
message.append('\n ' + _('Numeric digit (At least ' +
|
||||||
|
str(company_id.password_numeric) +
|
||||||
|
' numeric)')
|
||||||
|
)
|
||||||
|
if company_id.password_special and len(set('[~!@#$%^&*()_+{}":;\']+$'
|
||||||
|
).intersection(
|
||||||
|
password)) < company_id.password_numeric:
|
||||||
|
message.append('\n ' + _('Special character (At least ' +
|
||||||
|
str(company_id.password_special) +
|
||||||
|
' character of [ ~ ! @ # $ % ^ & * ( )_+ {'
|
||||||
|
' } " : ; \' ])')
|
||||||
|
)
|
||||||
|
if company_id.password_length and len(password) < \
|
||||||
|
company_id.password_length:
|
||||||
|
message = [_('Password must be %d characters or more.') %
|
||||||
|
company_id.password_length] + message
|
||||||
|
if len(message) > 0:
|
||||||
|
raise PassError('\r'.join(message))
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _password_has_expired(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.password_write_date:
|
||||||
|
return True
|
||||||
|
write_date = fields.Datetime.from_string(self.password_write_date)
|
||||||
|
today = fields.Datetime.from_string(fields.Datetime.now())
|
||||||
|
days = (today - write_date).days
|
||||||
|
return days > self.company_id.password_expiration
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_expire_password(self):
|
||||||
|
expiration = delta_now(days=+1)
|
||||||
|
for rec_id in self:
|
||||||
|
rec_id.mapped('partner_id').signup_prepare(
|
||||||
|
signup_type="reset", expiration=expiration
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _validate_pass_reset(self):
|
||||||
|
""" It provides validations before initiating a pass reset email
|
||||||
|
:raises: PassError on invalidated pass reset attempt
|
||||||
|
:return: True on allowed reset
|
||||||
|
"""
|
||||||
|
for rec_id in self:
|
||||||
|
pass_min = rec_id.company_id.password_minimum
|
||||||
|
if pass_min <= 0:
|
||||||
|
pass
|
||||||
|
write_date = fields.Datetime.from_string(
|
||||||
|
rec_id.password_write_date
|
||||||
|
)
|
||||||
|
delta = timedelta(hours=pass_min)
|
||||||
|
if write_date + delta > datetime.now():
|
||||||
|
raise PassError(
|
||||||
|
_('Passwords can only be reset every %d hour(s). '
|
||||||
|
'Please contact an administrator for assistance.') %
|
||||||
|
pass_min,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _check_password_history(self, password):
|
||||||
|
""" It validates proposed password against existing history
|
||||||
|
:raises: PassError on reused password
|
||||||
|
"""
|
||||||
|
crypt = self._crypt_context()
|
||||||
|
for rec_id in self:
|
||||||
|
recent_passes = rec_id.company_id.password_history
|
||||||
|
if recent_passes < 0:
|
||||||
|
recent_passes = rec_id.password_history_ids
|
||||||
|
else:
|
||||||
|
recent_passes = rec_id.password_history_ids[
|
||||||
|
0:recent_passes-1
|
||||||
|
]
|
||||||
|
if recent_passes.filtered(
|
||||||
|
lambda r: crypt.verify(password, r.password_crypt)):
|
||||||
|
raise PassError(
|
||||||
|
_('Cannot use the most recent %d passwords') %
|
||||||
|
rec_id.company_id.password_history
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _set_encrypted_password(self, encrypted):
|
||||||
|
""" It saves password crypt history for history rules """
|
||||||
|
super(ResUsers, self)._set_encrypted_password(encrypted)
|
||||||
|
self.write({
|
||||||
|
'password_history_ids': [(0, 0, {
|
||||||
|
'password_crypt': encrypted,
|
||||||
|
})],
|
||||||
|
})
|
25
addons/password_security/models/res_users_pass_history.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Copyright 2016 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from flectra import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResUsersPassHistory(models.Model):
|
||||||
|
_name = 'res.users.pass.history'
|
||||||
|
_description = 'Res Users Password History'
|
||||||
|
|
||||||
|
_order = 'user_id, date desc'
|
||||||
|
|
||||||
|
user_id = fields.Many2one(
|
||||||
|
string='User',
|
||||||
|
comodel_name='res.users',
|
||||||
|
ondelete='cascade',
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
password_crypt = fields.Char(
|
||||||
|
string='Encrypted Password',
|
||||||
|
)
|
||||||
|
date = fields.Datetime(
|
||||||
|
default=lambda s: fields.Datetime.now(),
|
||||||
|
index=True,
|
||||||
|
)
|
2
addons/password_security/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_res_users_pass_history,access_res_users_pass_history,model_res_users_pass_history,base.group_user,1,0,1,0
|
|
20
addons/password_security/security/res_users_pass_history.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2016 LasLabs Inc.
|
||||||
|
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<flectra>
|
||||||
|
|
||||||
|
<record id="res_users_pass_history_rule" model="ir.rule">
|
||||||
|
<field name="name">Res Users Pass History Access</field>
|
||||||
|
<field name="model_id"
|
||||||
|
ref="password_security.model_res_users_pass_history"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
('user_id', '=', user.id)
|
||||||
|
]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</flectra>
|
BIN
addons/password_security/static/description/icon.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
7
addons/password_security/tests/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from . import test_res_users
|
||||||
|
from . import test_password_security_home
|
||||||
|
from . import test_password_security_session
|
281
addons/password_security/tests/test_password_security_home.py
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2016 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from flectra.tests.common import TransactionCase
|
||||||
|
from flectra.http import Response
|
||||||
|
|
||||||
|
from ..controllers import main
|
||||||
|
|
||||||
|
|
||||||
|
IMPORT = 'flectra.addons.password_security.controllers.main'
|
||||||
|
|
||||||
|
|
||||||
|
class EndTestException(Exception):
|
||||||
|
""" It allows for isolation of resources by raise """
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponse(object):
|
||||||
|
def __new__(cls):
|
||||||
|
return mock.Mock(spec=Response)
|
||||||
|
|
||||||
|
|
||||||
|
class MockPassError(main.PassError):
|
||||||
|
def __init__(self):
|
||||||
|
super(MockPassError, self).__init__('Message')
|
||||||
|
|
||||||
|
|
||||||
|
class TestPasswordSecurityHome(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPasswordSecurityHome, self).setUp()
|
||||||
|
self.PasswordSecurityHome = main.PasswordSecurityHome
|
||||||
|
self.password_security_home = self.PasswordSecurityHome()
|
||||||
|
self.passwd = 'I am a password!'
|
||||||
|
self.qcontext = {
|
||||||
|
'password': self.passwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_assets(self):
|
||||||
|
""" It mocks and returns assets used by this controller """
|
||||||
|
methods = ['do_signup', 'web_login', 'web_auth_signup',
|
||||||
|
'web_auth_reset_password',
|
||||||
|
]
|
||||||
|
with mock.patch.multiple(
|
||||||
|
main.AuthSignupHome, **{m: mock.DEFAULT for m in methods}
|
||||||
|
) as _super:
|
||||||
|
mocks = {}
|
||||||
|
for method in methods:
|
||||||
|
mocks[method] = _super[method]
|
||||||
|
mocks[method].return_value = MockResponse()
|
||||||
|
with mock.patch('%s.request' % IMPORT) as request:
|
||||||
|
with mock.patch('%s.ensure_db' % IMPORT) as ensure:
|
||||||
|
with mock.patch('%s.http' % IMPORT) as http:
|
||||||
|
http.redirect_with_hash.return_value = \
|
||||||
|
MockResponse()
|
||||||
|
mocks.update({
|
||||||
|
'request': request,
|
||||||
|
'ensure_db': ensure,
|
||||||
|
'http': http,
|
||||||
|
})
|
||||||
|
yield mocks
|
||||||
|
|
||||||
|
def test_do_signup_check(self):
|
||||||
|
""" It should check password on user """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
check_password = assets['request'].env.user._check_password
|
||||||
|
check_password.side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.do_signup(self.qcontext)
|
||||||
|
check_password.assert_called_once_with(
|
||||||
|
self.passwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_do_signup_return(self):
|
||||||
|
""" It should return result of super """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
res = self.password_security_home.do_signup(self.qcontext)
|
||||||
|
self.assertEqual(assets['do_signup'](), res)
|
||||||
|
|
||||||
|
def test_web_login_ensure_db(self):
|
||||||
|
""" It should verify available db """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
assets['ensure_db'].side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_login()
|
||||||
|
|
||||||
|
def test_web_login_super(self):
|
||||||
|
""" It should call superclass w/ proper args """
|
||||||
|
expect_list = [1, 2, 3]
|
||||||
|
expect_dict = {'test1': 'good1', 'test2': 'good2'}
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
assets['web_login'].side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_login(
|
||||||
|
*expect_list, **expect_dict
|
||||||
|
)
|
||||||
|
assets['web_login'].assert_called_once_with(
|
||||||
|
*expect_list, **expect_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_no_post(self):
|
||||||
|
""" It should return immediate result of super when not POST """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
assets['request'].httprequest.method = 'GET'
|
||||||
|
assets['request'].session.authenticate.side_effect = \
|
||||||
|
EndTestException
|
||||||
|
res = self.password_security_home.web_login()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['web_login'](), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_authenticate(self):
|
||||||
|
""" It should attempt authentication to obtain uid """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
assets['request'].httprequest.method = 'POST'
|
||||||
|
authenticate = assets['request'].session.authenticate
|
||||||
|
request = assets['request']
|
||||||
|
authenticate.side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_login()
|
||||||
|
authenticate.assert_called_once_with(
|
||||||
|
request.session.db,
|
||||||
|
request.params['login'],
|
||||||
|
request.params['password'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_authenticate_fail(self):
|
||||||
|
""" It should return super result if failed auth """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
authenticate = assets['request'].session.authenticate
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
request.env['res.users'].sudo.side_effect = EndTestException
|
||||||
|
authenticate.return_value = False
|
||||||
|
res = self.password_security_home.web_login()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['web_login'](), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_get_user(self):
|
||||||
|
""" It should get the proper user as sudo """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
sudo = request.env['res.users'].sudo()
|
||||||
|
sudo.browse.side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_login()
|
||||||
|
sudo.browse.assert_called_once_with(
|
||||||
|
request.uid
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_valid_pass(self):
|
||||||
|
""" It should return parent result if pass isn't expired """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
user = request.env['res.users'].sudo().browse()
|
||||||
|
user.action_expire_password.side_effect = EndTestException
|
||||||
|
user._password_has_expired.return_value = False
|
||||||
|
res = self.password_security_home.web_login()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['web_login'](), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_login_expire_pass(self):
|
||||||
|
""" It should expire password if necessary """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
user = request.env['res.users'].sudo().browse()
|
||||||
|
user.action_expire_password.side_effect = EndTestException
|
||||||
|
user._password_has_expired.return_value = True
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_login()
|
||||||
|
|
||||||
|
def test_web_login_log_out_if_expired(self):
|
||||||
|
"""It should log out user if password expired"""
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
user = request.env['res.users'].sudo().browse()
|
||||||
|
user._password_has_expired.return_value = True
|
||||||
|
self.password_security_home.web_login()
|
||||||
|
|
||||||
|
logout_mock = request.session.logout
|
||||||
|
logout_mock.assert_called_once_with(keep_db=True)
|
||||||
|
|
||||||
|
def test_web_login_redirect(self):
|
||||||
|
""" It should redirect w/ hash to reset after expiration """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
request = assets['request']
|
||||||
|
request.httprequest.method = 'POST'
|
||||||
|
user = request.env['res.users'].sudo().browse()
|
||||||
|
user._password_has_expired.return_value = True
|
||||||
|
res = self.password_security_home.web_login()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['http'].redirect_with_hash(), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_auth_signup_valid(self):
|
||||||
|
""" It should return super if no errors """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
res = self.password_security_home.web_auth_signup()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['web_auth_signup'](), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_auth_signup_invalid_qcontext(self):
|
||||||
|
""" It should catch PassError and get signup qcontext """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with mock.patch.object(
|
||||||
|
main.AuthSignupHome, 'get_auth_signup_qcontext',
|
||||||
|
) as qcontext:
|
||||||
|
assets['web_auth_signup'].side_effect = MockPassError
|
||||||
|
qcontext.side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_home.web_auth_signup()
|
||||||
|
|
||||||
|
def test_web_auth_signup_invalid_render(self):
|
||||||
|
""" It should render & return signup form on invalid """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with mock.patch.object(
|
||||||
|
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
|
||||||
|
) as qcontext:
|
||||||
|
assets['web_auth_signup'].side_effect = MockPassError
|
||||||
|
res = self.password_security_home.web_auth_signup()
|
||||||
|
assets['request'].render.assert_called_once_with(
|
||||||
|
'auth_signup.signup', qcontext(),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
assets['request'].render(), res,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_auth_reset_password_fail_login(self):
|
||||||
|
""" It should raise from failed _validate_pass_reset by login """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with mock.patch.object(
|
||||||
|
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
|
||||||
|
) as qcontext:
|
||||||
|
qcontext['login'] = 'login'
|
||||||
|
search = assets['request'].env.sudo().search
|
||||||
|
assets['request'].httprequest.method = 'POST'
|
||||||
|
user = mock.MagicMock()
|
||||||
|
user._validate_pass_reset.side_effect = MockPassError
|
||||||
|
search.return_value = user
|
||||||
|
with self.assertRaises(MockPassError):
|
||||||
|
self.password_security_home.web_auth_reset_password()
|
||||||
|
|
||||||
|
def test_web_auth_reset_password_fail_email(self):
|
||||||
|
""" It should raise from failed _validate_pass_reset by email """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with mock.patch.object(
|
||||||
|
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
|
||||||
|
) as qcontext:
|
||||||
|
qcontext['login'] = 'login'
|
||||||
|
search = assets['request'].env.sudo().search
|
||||||
|
assets['request'].httprequest.method = 'POST'
|
||||||
|
user = mock.MagicMock()
|
||||||
|
user._validate_pass_reset.side_effect = MockPassError
|
||||||
|
search.side_effect = [[], user]
|
||||||
|
with self.assertRaises(MockPassError):
|
||||||
|
self.password_security_home.web_auth_reset_password()
|
||||||
|
|
||||||
|
def test_web_auth_reset_password_success(self):
|
||||||
|
""" It should return parent response on no validate errors """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
with mock.patch.object(
|
||||||
|
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
|
||||||
|
) as qcontext:
|
||||||
|
qcontext['login'] = 'login'
|
||||||
|
assets['request'].httprequest.method = 'POST'
|
||||||
|
res = self.password_security_home.web_auth_reset_password()
|
||||||
|
self.assertEqual(
|
||||||
|
assets['web_auth_reset_password'](), res,
|
||||||
|
)
|
@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2016 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from flectra.tests.common import TransactionCase
|
||||||
|
|
||||||
|
from ..controllers import main
|
||||||
|
|
||||||
|
|
||||||
|
IMPORT = 'flectra.addons.password_security.controllers.main'
|
||||||
|
|
||||||
|
|
||||||
|
class EndTestException(Exception):
|
||||||
|
""" It allows for isolation of resources by raise """
|
||||||
|
|
||||||
|
|
||||||
|
class TestPasswordSecuritySession(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPasswordSecuritySession, self).setUp()
|
||||||
|
self.PasswordSecuritySession = main.PasswordSecuritySession
|
||||||
|
self.password_security_session = self.PasswordSecuritySession()
|
||||||
|
self.passwd = 'I am a password!'
|
||||||
|
self.fields = [
|
||||||
|
{'name': 'new_password', 'value': self.passwd},
|
||||||
|
]
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_assets(self):
|
||||||
|
""" It mocks and returns assets used by this controller """
|
||||||
|
with mock.patch('%s.request' % IMPORT) as request:
|
||||||
|
yield {
|
||||||
|
'request': request,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_change_password_check(self):
|
||||||
|
""" It should check password on request user """
|
||||||
|
with self.mock_assets() as assets:
|
||||||
|
check_password = assets['request'].env.user._check_password
|
||||||
|
check_password.side_effect = EndTestException
|
||||||
|
with self.assertRaises(EndTestException):
|
||||||
|
self.password_security_session.change_password(self.fields)
|
||||||
|
check_password.assert_called_once_with(
|
||||||
|
self.passwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_password_return(self):
|
||||||
|
""" It should return result of super """
|
||||||
|
with self.mock_assets():
|
||||||
|
with mock.patch.object(main.Session, 'change_password') as chg:
|
||||||
|
res = self.password_security_session.change_password(
|
||||||
|
self.fields
|
||||||
|
)
|
||||||
|
self.assertEqual(chg(), res)
|
148
addons/password_security/tests/test_res_users.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from flectra.tests.common import TransactionCase
|
||||||
|
|
||||||
|
from ..exceptions import PassError
|
||||||
|
|
||||||
|
|
||||||
|
class TestResUsers(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestResUsers, self).setUp()
|
||||||
|
self.login = 'foslabs@example.com'
|
||||||
|
self.partner_vals = {
|
||||||
|
'name': 'Partner',
|
||||||
|
'is_company': False,
|
||||||
|
'email': self.login,
|
||||||
|
}
|
||||||
|
self.password = 'asdQWE123$%^'
|
||||||
|
self.main_comp = self.env.ref('base.main_company')
|
||||||
|
self.vals = {
|
||||||
|
'name': 'User',
|
||||||
|
'login': self.login,
|
||||||
|
'password': self.password,
|
||||||
|
'company_id': self.main_comp.id
|
||||||
|
}
|
||||||
|
self.model_obj = self.env['res.users']
|
||||||
|
|
||||||
|
def _new_record(self):
|
||||||
|
partner_id = self.env['res.partner'].create(self.partner_vals)
|
||||||
|
self.vals['partner_id'] = partner_id.id
|
||||||
|
return self.model_obj.create(self.vals)
|
||||||
|
|
||||||
|
def test_password_write_date_is_saved_on_create(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
self.assertTrue(
|
||||||
|
rec_id.password_write_date,
|
||||||
|
'Password write date was not saved to db.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_password_write_date_is_updated_on_write(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
old_write_date = rec_id.password_write_date
|
||||||
|
time.sleep(2)
|
||||||
|
rec_id.write({'password': 'asdQWE123$%^2'})
|
||||||
|
rec_id.refresh()
|
||||||
|
new_write_date = rec_id.password_write_date
|
||||||
|
self.assertNotEqual(
|
||||||
|
old_write_date, new_write_date,
|
||||||
|
'Password write date was not updated on write.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_does_not_update_write_date_if_password_unchanged(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
old_write_date = rec_id.password_write_date
|
||||||
|
time.sleep(2)
|
||||||
|
rec_id.write({'name': 'Luser'})
|
||||||
|
rec_id.refresh()
|
||||||
|
new_write_date = rec_id.password_write_date
|
||||||
|
self.assertEqual(
|
||||||
|
old_write_date, new_write_date,
|
||||||
|
'Password not changed but write date updated anyway.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_password_returns_true_for_valid_password(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
self.assertTrue(
|
||||||
|
rec_id._check_password('asdQWE123$%^3'),
|
||||||
|
'Password is valid but check failed.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_password_raises_error_for_invalid_password(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
with self.assertRaises(PassError):
|
||||||
|
rec_id._check_password('password')
|
||||||
|
|
||||||
|
def test_save_password_crypt(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
self.assertEqual(
|
||||||
|
1, len(rec_id.password_history_ids),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_password_crypt(self):
|
||||||
|
""" It should raise PassError if previously used """
|
||||||
|
rec_id = self._new_record()
|
||||||
|
with self.assertRaises(PassError):
|
||||||
|
rec_id.write({'password': self.password})
|
||||||
|
|
||||||
|
def test_password_is_expired_if_record_has_no_write_date(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
rec_id.write({'password_write_date': None})
|
||||||
|
rec_id.refresh()
|
||||||
|
self.assertTrue(
|
||||||
|
rec_id._password_has_expired(),
|
||||||
|
'Record has no password write date but check failed.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_an_old_password_is_expired(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
old_write_date = '1970-01-01 00:00:00'
|
||||||
|
rec_id.write({'password_write_date': old_write_date})
|
||||||
|
rec_id.refresh()
|
||||||
|
self.assertTrue(
|
||||||
|
rec_id._password_has_expired(),
|
||||||
|
'Password is out of date but check failed.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_new_password_is_not_expired(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
self.assertFalse(
|
||||||
|
rec_id._password_has_expired(),
|
||||||
|
'Password was just created but has already expired.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_expire_password_generates_token(self):
|
||||||
|
rec_id = self._new_record()
|
||||||
|
rec_id.sudo().action_expire_password()
|
||||||
|
rec_id.refresh()
|
||||||
|
token = rec_id.partner_id.signup_token
|
||||||
|
self.assertTrue(
|
||||||
|
token,
|
||||||
|
'A token was not generated.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validate_pass_reset_error(self):
|
||||||
|
""" It should throw PassError on reset inside min threshold """
|
||||||
|
rec_id = self._new_record()
|
||||||
|
with self.assertRaises(PassError):
|
||||||
|
rec_id._validate_pass_reset()
|
||||||
|
|
||||||
|
def test_validate_pass_reset_allow(self):
|
||||||
|
""" It should allow reset pass when outside threshold """
|
||||||
|
rec_id = self._new_record()
|
||||||
|
rec_id.password_write_date = '2016-01-01'
|
||||||
|
self.assertEqual(
|
||||||
|
True, rec_id._validate_pass_reset(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validate_pass_reset_zero(self):
|
||||||
|
""" It should allow reset pass when <= 0 """
|
||||||
|
rec_id = self._new_record()
|
||||||
|
rec_id.company_id.password_minimum = 0
|
||||||
|
self.assertEqual(
|
||||||
|
True, rec_id._validate_pass_reset(),
|
||||||
|
)
|
42
addons/password_security/views/res_company_view.xml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2015 LasLabs Inc.
|
||||||
|
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<flectra>
|
||||||
|
|
||||||
|
<record id="view_company_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.company.form</field>
|
||||||
|
<field name="model">res.company</field>
|
||||||
|
<field name="inherit_id" ref="base.view_company_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Password Policy">
|
||||||
|
<group>
|
||||||
|
<group string="Timings">
|
||||||
|
<field name="password_expiration" />
|
||||||
|
<field name="password_minimum" />
|
||||||
|
</group>
|
||||||
|
<group string="Extra">
|
||||||
|
<field name="password_length" />
|
||||||
|
<field name="password_history" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group name="chars_grp" string="Required Characters">
|
||||||
|
<group>
|
||||||
|
<field name="password_lower" />
|
||||||
|
<field name="password_upper" />
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="password_numeric" />
|
||||||
|
<field name="password_special" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</flectra>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.1 KiB |
@ -278,7 +278,7 @@
|
|||||||
<field name="project_id" ref="project.project_project_3"/>
|
<field name="project_id" ref="project.project_project_3"/>
|
||||||
<field name="name">User Interface design</field>
|
<field name="name">User Interface design</field>
|
||||||
<field name="stage_id" ref="project_stage_2"/>
|
<field name="stage_id" ref="project_stage_2"/>
|
||||||
<field name="date_start">2011-02-06</field>
|
<field name="date_start">2016-08-25</field>
|
||||||
<field name="tag_ids" eval="[(6, 0, [
|
<field name="tag_ids" eval="[(6, 0, [
|
||||||
ref('project_tags_03')])]"/>
|
ref('project_tags_03')])]"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -25,10 +25,8 @@
|
|||||||
Good luck!
|
Good luck!
|
||||||
</p></i>
|
</p></i>
|
||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<img class="signature" src="/web_planner/static/src/img/fabien_signature.png"/>
|
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.3 KiB |
@ -627,6 +627,14 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="view_project_task_gantt" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<gantt date_start="date_start" date_stop="date_end" string="Tasks"/>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="analytic_account_inherited_form" model="ir.ui.view">
|
<record id="analytic_account_inherited_form" model="ir.ui.view">
|
||||||
<field name="name">account.analytic.account.form.inherit</field>
|
<field name="name">account.analytic.account.form.inherit</field>
|
||||||
<field name="model">account.analytic.account</field>
|
<field name="model">account.analytic.account</field>
|
||||||
@ -645,7 +653,7 @@
|
|||||||
<record id="action_view_task" model="ir.actions.act_window">
|
<record id="action_view_task" model="ir.actions.act_window">
|
||||||
<field name="name">Tasks</field>
|
<field name="name">Tasks</field>
|
||||||
<field name="res_model">project.task</field>
|
<field name="res_model">project.task</field>
|
||||||
<field name="view_mode">kanban,tree,form,calendar,pivot,graph</field>
|
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,gantt</field>
|
||||||
<field name="context">{'search_default_my_tasks': 1}</field>
|
<field name="context">{'search_default_my_tasks': 1}</field>
|
||||||
<field name="search_view_id" ref="view_task_search_form"/>
|
<field name="search_view_id" ref="view_task_search_form"/>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.5 KiB |
@ -2,8 +2,8 @@
|
|||||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import openerp
|
import flectra
|
||||||
from openerp import api, SUPERUSER_ID
|
from flectra import api, SUPERUSER_ID
|
||||||
|
|
||||||
from . import models # noqa
|
from . import models # noqa
|
||||||
from . import report # noqa
|
from . import report # noqa
|
||||||
|
@ -22,10 +22,8 @@
|
|||||||
to inventory reduction and better efficiencies in your daily operations.
|
to inventory reduction and better efficiencies in your daily operations.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<img class="signature mb8" src="/web_planner/static/src/img/fabien_signature.png"/>
|
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.2 KiB |
73
addons/web/static/lib/jquery.ganttView/README.markdown
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
jQuery.ganttView
|
||||||
|
================
|
||||||
|
|
||||||
|
The jQuery.ganttView plugin is a very lightweight plugin for creating a Gantt chart in plain HTML...no vector graphics or images required. The plugin supports dragging and resizing the Gantt blocks and callbacks to trap the updated data.
|
||||||
|
|
||||||
|
[![Sample Gantt](https://raw.githubusercontent.com/thegrubbsian/jquery.ganttView/master/example/jquery-ganttview.png) A sample chart](http://thegrubbsian.github.io/jquery.ganttView/example/index.html)
|
||||||
|
|
||||||
|
|
||||||
|
Browser Compatibility
|
||||||
|
---------------------
|
||||||
|
Currently the plugin has been tested, and is working in: FF 3.5+, Chrome 5+, Safari 4+, IE8+. There are minor issues in IE7 and I haven't even attempted to use it in IE6. If you encounter any issues with any version of Internet Explorer and would like to contribute CSS fixes please do so, several people have asked for IE6 support.
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
The plugin depends on the following libraries:
|
||||||
|
|
||||||
|
- jQuery 1.7 or higher
|
||||||
|
- jQuery-UI 1.8 or higher
|
||||||
|
- date.js
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
Forthcoming...
|
||||||
|
|
||||||
|
|
||||||
|
Contribution Guidelines
|
||||||
|
------------
|
||||||
|
The internal roadmap for the plugin is detailed in the project wiki. If you're interested in features outside of what's described there, we'd be interested to discuss pull requests that would add these features. If you like the plugin, feel free to fork it and submit your patches back.
|
||||||
|
|
||||||
|
**Guidelines:** If you'd like to offer a new feature please help us out by submitting the pull request with only the fewest changes necessary.
|
||||||
|
|
||||||
|
Ideal: Fork the project, apply just the individual changes to the individual files effected, submit pull request.
|
||||||
|
|
||||||
|
Those pull requests can usually be automatically merged and closed through the site here.
|
||||||
|
|
||||||
|
If your pull request includes things like:
|
||||||
|
|
||||||
|
- changes to dependencies or where they're hosted
|
||||||
|
- stylistic modifications
|
||||||
|
- moving project files to different directories
|
||||||
|
- more than one new feature / functional change
|
||||||
|
|
||||||
|
one of us will have to do the work of carving out just the feature being pulled. Your request is likely to sit unmerged for a while if that's the case.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
The jQuery.ganttView plugin may be used free of charge under the conditions
|
||||||
|
of the following license:
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
145
addons/web/static/lib/jquery.ganttView/date.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* @version: 1.0 Alpha-1
|
||||||
|
* @author: Coolite Inc. http://www.coolite.com/
|
||||||
|
* @date: 2008-05-13
|
||||||
|
* @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
|
||||||
|
* @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
|
||||||
|
* @website: http://www.datejs.com/
|
||||||
|
*/
|
||||||
|
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]};
|
||||||
|
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,p=function(s,l){if(!l){l=2;}
|
||||||
|
return("000"+s).slice(l*-1);};$P.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};$P.setTimeToNow=function(){var n=new Date();this.setHours(n.getHours());this.setMinutes(n.getMinutes());this.setSeconds(n.getSeconds());this.setMilliseconds(n.getMilliseconds());return this;};$D.today=function(){return new Date().clearTime();};$D.compare=function(date1,date2){if(isNaN(date1)||isNaN(date2)){throw new Error(date1+" - "+date2);}else if(date1 instanceof Date&&date2 instanceof Date){return(date1<date2)?-1:(date1>date2)?1:0;}else{throw new TypeError(date1+" - "+date2);}};$D.equals=function(date1,date2){return(date1.compareTo(date2)===0);};$D.getDayNumberFromName=function(name){var n=$C.dayNames,m=$C.abbreviatedDayNames,o=$C.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s||o[i].toLowerCase()==s){return i;}}
|
||||||
|
return-1;};$D.getMonthNumberFromName=function(name){var n=$C.monthNames,m=$C.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
|
||||||
|
return-1;};$D.isLeapYear=function(year){return((year%4===0&&year%100!==0)||year%400===0);};$D.getDaysInMonth=function(year,month){return[31,($D.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};$D.getTimezoneAbbreviation=function(offset){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].offset===offset){return z[i].name;}}
|
||||||
|
return null;};$D.getTimezoneOffset=function(name){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].name===name.toUpperCase()){return z[i].offset;}}
|
||||||
|
return null;};$P.clone=function(){return new Date(this.getTime());};$P.compareTo=function(date){return Date.compare(this,date);};$P.equals=function(date){return Date.equals(this,date||new Date());};$P.between=function(start,end){return this.getTime()>=start.getTime()&&this.getTime()<=end.getTime();};$P.isAfter=function(date){return this.compareTo(date||new Date())===1;};$P.isBefore=function(date){return(this.compareTo(date||new Date())===-1);};$P.isToday=function(){return this.isSameDay(new Date());};$P.isSameDay=function(date){return this.clone().clearTime().equals(date.clone().clearTime());};$P.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};$P.addSeconds=function(value){return this.addMilliseconds(value*1000);};$P.addMinutes=function(value){return this.addMilliseconds(value*60000);};$P.addHours=function(value){return this.addMilliseconds(value*3600000);};$P.addDays=function(value){this.setDate(this.getDate()+value);return this;};$P.addWeeks=function(value){return this.addDays(value*7);};$P.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,$D.getDaysInMonth(this.getFullYear(),this.getMonth())));return this;};$P.addYears=function(value){return this.addMonths(value*12);};$P.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
|
||||||
|
var x=config;if(x.milliseconds){this.addMilliseconds(x.milliseconds);}
|
||||||
|
if(x.seconds){this.addSeconds(x.seconds);}
|
||||||
|
if(x.minutes){this.addMinutes(x.minutes);}
|
||||||
|
if(x.hours){this.addHours(x.hours);}
|
||||||
|
if(x.weeks){this.addWeeks(x.weeks);}
|
||||||
|
if(x.months){this.addMonths(x.months);}
|
||||||
|
if(x.years){this.addYears(x.years);}
|
||||||
|
if(x.days){this.addDays(x.days);}
|
||||||
|
return this;};var $y,$m,$d;$P.getWeek=function(){var a,b,c,d,e,f,g,n,s,w;$y=(!$y)?this.getFullYear():$y;$m=(!$m)?this.getMonth()+1:$m;$d=(!$d)?this.getDate():$d;if($m<=2){a=$y-1;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=0;f=$d-1+(31*($m-1));}else{a=$y;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=s+1;f=$d+((153*($m-3)+2)/5)+58+s;}
|
||||||
|
g=(a+b)%7;d=(f+g-e)%7;n=(f+3-d)|0;if(n<0){w=53-((g-s)/5|0);}else if(n>364+s){w=1;}else{w=(n/7|0)+1;}
|
||||||
|
$y=$m=$d=null;return w;};$P.getISOWeek=function(){$y=this.getUTCFullYear();$m=this.getUTCMonth()+1;$d=this.getUTCDate();return p(this.getWeek());};$P.setWeek=function(n){return this.moveToDayOfWeek(1).addWeeks(n-this.getWeek());};$D._validate=function(n,min,max,name){if(typeof n=="undefined"){return false;}else if(typeof n!="number"){throw new TypeError(n+" is not a Number.");}else if(n<min||n>max){throw new RangeError(n+" is not a valid value for "+name+".");}
|
||||||
|
return true;};$D.validateMillisecond=function(value){return $D._validate(value,0,999,"millisecond");};$D.validateSecond=function(value){return $D._validate(value,0,59,"second");};$D.validateMinute=function(value){return $D._validate(value,0,59,"minute");};$D.validateHour=function(value){return $D._validate(value,0,23,"hour");};$D.validateDay=function(value,year,month){return $D._validate(value,1,$D.getDaysInMonth(year,month),"day");};$D.validateMonth=function(value){return $D._validate(value,0,11,"month");};$D.validateYear=function(value){return $D._validate(value,0,9999,"year");};$P.set=function(config){if($D.validateMillisecond(config.millisecond)){this.addMilliseconds(config.millisecond-this.getMilliseconds());}
|
||||||
|
if($D.validateSecond(config.second)){this.addSeconds(config.second-this.getSeconds());}
|
||||||
|
if($D.validateMinute(config.minute)){this.addMinutes(config.minute-this.getMinutes());}
|
||||||
|
if($D.validateHour(config.hour)){this.addHours(config.hour-this.getHours());}
|
||||||
|
if($D.validateMonth(config.month)){this.addMonths(config.month-this.getMonth());}
|
||||||
|
if($D.validateYear(config.year)){this.addYears(config.year-this.getFullYear());}
|
||||||
|
if($D.validateDay(config.day,this.getFullYear(),this.getMonth())){this.addDays(config.day-this.getDate());}
|
||||||
|
if(config.timezone){this.setTimezone(config.timezone);}
|
||||||
|
if(config.timezoneOffset){this.setTimezoneOffset(config.timezoneOffset);}
|
||||||
|
if(config.week&&$D._validate(config.week,0,53,"week")){this.setWeek(config.week);}
|
||||||
|
return this;};$P.moveToFirstDayOfMonth=function(){return this.set({day:1});};$P.moveToLastDayOfMonth=function(){return this.set({day:$D.getDaysInMonth(this.getFullYear(),this.getMonth())});};$P.moveToNthOccurrence=function(dayOfWeek,occurrence){var shift=0;if(occurrence>0){shift=occurrence-1;}
|
||||||
|
else if(occurrence===-1){this.moveToLastDayOfMonth();if(this.getDay()!==dayOfWeek){this.moveToDayOfWeek(dayOfWeek,-1);}
|
||||||
|
return this;}
|
||||||
|
return this.moveToFirstDayOfMonth().addDays(-1).moveToDayOfWeek(dayOfWeek,+1).addWeeks(shift);};$P.moveToDayOfWeek=function(dayOfWeek,orient){var diff=(dayOfWeek-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};$P.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};$P.getOrdinalNumber=function(){return Math.ceil((this.clone().clearTime()-new Date(this.getFullYear(),0,1))/86400000)+1;};$P.getTimezone=function(){return $D.getTimezoneAbbreviation(this.getUTCOffset());};$P.setTimezoneOffset=function(offset){var here=this.getTimezoneOffset(),there=Number(offset)*-6/10;return this.addMinutes(there-here);};$P.setTimezone=function(offset){return this.setTimezoneOffset($D.getTimezoneOffset(offset));};$P.hasDaylightSavingTime=function(){return(Date.today().set({month:0,day:1}).getTimezoneOffset()!==Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.isDaylightSavingTime=function(){return(this.hasDaylightSavingTime()&&new Date().getTimezoneOffset()===Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r.charAt(0)+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};$P.getElapsed=function(date){return(date||new Date())-this;};if(!$P.toISOString){$P.toISOString=function(){function f(n){return n<10?'0'+n:n;}
|
||||||
|
return'"'+this.getUTCFullYear()+'-'+
|
||||||
|
f(this.getUTCMonth()+1)+'-'+
|
||||||
|
f(this.getUTCDate())+'T'+
|
||||||
|
f(this.getUTCHours())+':'+
|
||||||
|
f(this.getUTCMinutes())+':'+
|
||||||
|
f(this.getUTCSeconds())+'Z"';};}
|
||||||
|
$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
|
||||||
|
var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");}
|
||||||
|
x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}());
|
||||||
|
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());}
|
||||||
|
return false;};$P.at=function(time){return(typeof time==="string")?$D.parse(this.toString("d")+" "+time):this.set(time);};$N.fromNow=$N.after=function(date){var c={};c[this._dateElement]=this;return((!date)?new Date():date.clone()).add(c);};$N.ago=$N.before=function(date){var c={};c[this._dateElement]=this*-1;return((!date)?new Date():date.clone()).add(c);};var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),pxf=("Milliseconds Seconds Minutes Hours Date Week Month FullYear").split(/\s/),nth=("final first second third fourth fifth").split(/\s/),de;$P.toObject=function(){var o={};for(var i=0;i<px.length;i++){o[px[i].toLowerCase()]=this["get"+pxf[i]]();}
|
||||||
|
return o;};$D.fromObject=function(config){config.week=null;return Date.today().set(config);};var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
|
||||||
|
if(this._nth!==null){if(this._isSecond){this.addSeconds(this._orient*-1);}
|
||||||
|
this._isSecond=false;var ntemp=this._nth;this._nth=null;var temp=this.clone().moveToLastDayOfMonth();this.moveToNthOccurrence(n,ntemp);if(this>temp){throw new RangeError($D.getDayName(n)+" does not occur "+ntemp+" times in the month of "+$D.getMonthName(temp.getMonth())+" "+temp.getFullYear()+".");}
|
||||||
|
return this;}
|
||||||
|
return this.moveToDayOfWeek(n,this._orient);};};var sdf=function(n){return function(){var t=$D.today(),shift=n-t.getDay();if(n===0&&$C.firstDayOfWeek===1&&t.getDay()!==0){shift=shift+7;}
|
||||||
|
return t.addDays(shift);};};for(var i=0;i<dx.length;i++){$D[dx[i].toUpperCase()]=$D[dx[i].toUpperCase().substring(0,3)]=i;$D[dx[i]]=$D[dx[i].substring(0,3)]=sdf(i);$P[dx[i]]=$P[dx[i].substring(0,3)]=df(i);}
|
||||||
|
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
|
||||||
|
return this.moveToMonth(n,this._orient);};};var smf=function(n){return function(){return $D.today().set({month:n,day:1});};};for(var j=0;j<mx.length;j++){$D[mx[j].toUpperCase()]=$D[mx[j].toUpperCase().substring(0,3)]=j;$D[mx[j]]=$D[mx[j].substring(0,3)]=smf(j);$P[mx[j]]=$P[mx[j].substring(0,3)]=mf(j);}
|
||||||
|
var ef=function(j){return function(){if(this._isSecond){this._isSecond=false;return this;}
|
||||||
|
if(this._same){this._same=this._is=false;var o1=this.toObject(),o2=(arguments[0]||new Date()).toObject(),v="",k=j.toLowerCase();for(var m=(px.length-1);m>-1;m--){v=px[m].toLowerCase();if(o1[v]!=o2[v]){return false;}
|
||||||
|
if(k==v){break;}}
|
||||||
|
return true;}
|
||||||
|
if(j.substring(j.length-1)!="s"){j+="s";}
|
||||||
|
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$P[de]=$P[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}
|
||||||
|
$P._ss=ef("Second");var nthfn=function(n){return function(dayOfWeek){if(this._same){return this._ss(arguments[0]);}
|
||||||
|
if(dayOfWeek||dayOfWeek===0){return this.moveToNthOccurrence(dayOfWeek,n);}
|
||||||
|
this._nth=n;if(n===2&&(dayOfWeek===undefined||dayOfWeek===null)){this._isSecond=true;return this.addSeconds(this._orient);}
|
||||||
|
return this;};};for(var l=0;l<nth.length;l++){$P[nth[l]]=(l===0)?nthfn(-1):nthfn(l);}}());
|
||||||
|
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
|
||||||
|
break;}
|
||||||
|
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
|
||||||
|
rx.push(r[0]);s=r[1];}
|
||||||
|
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
|
||||||
|
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
|
||||||
|
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
|
||||||
|
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
|
||||||
|
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
|
||||||
|
try{r=(px[i].call(this,s));}catch(e){r=null;}
|
||||||
|
if(r){return r;}}
|
||||||
|
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
|
||||||
|
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
|
||||||
|
rx.push(r[0]);s=r[1];}
|
||||||
|
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
|
||||||
|
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
|
||||||
|
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
|
||||||
|
s=q[1];}
|
||||||
|
if(!r){throw new $P.Exception(s);}
|
||||||
|
if(q){throw new $P.Exception(q[1]);}
|
||||||
|
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
|
||||||
|
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
|
||||||
|
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
|
||||||
|
if(!last&&q[1].length===0){last=true;}
|
||||||
|
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
|
||||||
|
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
|
||||||
|
if(rx[1].length<best[1].length){best=rx;}
|
||||||
|
if(best[1].length===0){break;}}
|
||||||
|
if(best[0].length===0){return best;}
|
||||||
|
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
|
||||||
|
best[1]=q[1];}
|
||||||
|
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
|
||||||
|
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
|
||||||
|
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
|
||||||
|
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo;var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
|
||||||
|
return rx;};$D.Grammar={};$D.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=(s.length==3)?"jan feb mar apr may jun jul aug sep oct nov dec".indexOf(s)/4:Number(s)-1;};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<$C.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
|
||||||
|
var now=new Date();if((this.hour||this.minute)&&(!this.month&&!this.year&&!this.day)){this.day=now.getDate();}
|
||||||
|
if(!this.year){this.year=now.getFullYear();}
|
||||||
|
if(!this.month&&this.month!==0){this.month=now.getMonth();}
|
||||||
|
if(!this.day){this.day=1;}
|
||||||
|
if(!this.hour){this.hour=0;}
|
||||||
|
if(!this.minute){this.minute=0;}
|
||||||
|
if(!this.second){this.second=0;}
|
||||||
|
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
|
||||||
|
if(this.day>$D.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
|
||||||
|
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
|
||||||
|
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
|
||||||
|
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
|
||||||
|
var today=$D.today();if(this.now&&!this.unit&&!this.operator){return new Date();}else if(this.now){today=new Date();}
|
||||||
|
var expression=!!(this.days&&this.days!==null||this.orient||this.operator);var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(!this.now&&"hour minute second".indexOf(this.unit)!=-1){today.setTimeToNow();}
|
||||||
|
if(this.month||this.month===0){if("year day hour minute second".indexOf(this.unit)!=-1){this.value=this.month+1;this.month=null;expression=true;}}
|
||||||
|
if(!expression&&this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(!this.month){this.month=temp.getMonth();}
|
||||||
|
this.year=temp.getFullYear();}
|
||||||
|
if(expression&&this.weekday&&this.unit!="month"){this.unit="day";gap=($D.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
|
||||||
|
if(this.month&&this.unit=="day"&&this.operator){this.value=(this.month+1);this.month=null;}
|
||||||
|
if(this.value!=null&&this.month!=null&&this.year!=null){this.day=this.value*1;}
|
||||||
|
if(this.month&&!this.day&&this.value){today.set({day:this.value*1});if(!expression){this.day=this.value*1;}}
|
||||||
|
if(!this.month&&this.value&&this.unit=="month"&&!this.now){this.month=this.value;expression=true;}
|
||||||
|
if(expression&&(this.month||this.month===0)&&this.unit!="year"){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
|
||||||
|
if(!this.unit){this.unit="day";}
|
||||||
|
if(!this.value&&this.operator&&this.operator!==null&&this[this.unit+"s"]&&this[this.unit+"s"]!==null){this[this.unit+"s"]=this[this.unit+"s"]+((this.operator=="add")?1:-1)+(this.value||0)*orient;}else if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
|
||||||
|
this[this.unit+"s"]=this.value*orient;}
|
||||||
|
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
|
||||||
|
if(this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(temp.getMonth()!==today.getMonth()){this.month=temp.getMonth();}}
|
||||||
|
if((this.month||this.month===0)&&!this.day){this.day=1;}
|
||||||
|
if(!this.orient&&!this.operator&&this.unit=="week"&&this.value&&!this.day&&!this.month){return Date.today().setWeek(this.value);}
|
||||||
|
if(expression&&this.timezone&&this.day&&this.days){this.day=this.days;}
|
||||||
|
return(expression)?today.add(this):today.set(this);}};var _=$D.Parsing.Operators,g=$D.Grammar,t=$D.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|@|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=$C.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
|
||||||
|
fn=_C[keys]=_.any.apply(null,px);}
|
||||||
|
return fn;};g.ctoken2=function(key){return _.rtoken($C.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.m,g.s],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("second minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[$C.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw $D.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
|
||||||
|
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["\"yyyy-MM-ddTHH:mm:ssZ\"","yyyy-MM-ddTHH:mm:ssZ","yyyy-MM-ddTHH:mm:ssz","yyyy-MM-ddTHH:mm:ss","yyyy-MM-ddTHH:mmZ","yyyy-MM-ddTHH:mmz","yyyy-MM-ddTHH:mm","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","MMddyyyy","ddMMyyyy","Mddyyyy","ddMyyyy","Mdyyyy","dMyyyy","yyyy","Mdyy","dMyy","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
|
||||||
|
return g._start.call({},s);};$D._parse=$D.parse;$D.parse=function(s){var r=null;if(!s){return null;}
|
||||||
|
if(s instanceof Date){return s;}
|
||||||
|
try{r=$D.Grammar.start.call({},s.replace(/^\s*(\S*(\s+\S+)*)\s*$/,"$1"));}catch(e){return null;}
|
||||||
|
return((r[1].length===0)?r[0]:null);};$D.getParseFunction=function(fx){var fn=$D.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
|
||||||
|
return((r[1].length===0)?r[0]:null);};};$D.parseExact=function(s,fx){return $D.getParseFunction(fx)(s);};}());
|
137
addons/web/static/lib/jquery.ganttView/jquery.ganttView.css
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
jQuery.ganttView v.0.8.0
|
||||||
|
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
|
||||||
|
MIT License Applies
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.ganttview-hzheader-month,
|
||||||
|
div.ganttview-hzheader-day,
|
||||||
|
div.ganttview-vtheader,
|
||||||
|
div.ganttview-vtheader-item-name,
|
||||||
|
div.ganttview-vtheader-series,
|
||||||
|
div.ganttview-grid,
|
||||||
|
div.ganttview-grid-row-cell {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-hzheader-month,
|
||||||
|
div.ganttview-hzheader-day {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-grid-row-cell.last,
|
||||||
|
div.ganttview-hzheader-day.last,
|
||||||
|
div.ganttview-hzheader-month.last {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal Header */
|
||||||
|
|
||||||
|
div.ganttview-hzheader-month {
|
||||||
|
width: 60px;
|
||||||
|
height: 20px;
|
||||||
|
border-right: 1px solid #d0d0d0;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-hzheader-day {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
border-top: 1px solid #d0d0d0;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical Header */
|
||||||
|
|
||||||
|
div.ganttview-vtheader {
|
||||||
|
margin-top: 41px;
|
||||||
|
width: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-vtheader-item {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-vtheader-item-name {
|
||||||
|
width: 100px;
|
||||||
|
padding-left: 5px;
|
||||||
|
border-top: 1px solid #d0d0d0;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-vtheader-series-name {
|
||||||
|
width: 130px;
|
||||||
|
height: 31px;
|
||||||
|
border-top: 1px solid #d0d0d0;
|
||||||
|
line-height: 16px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slider */
|
||||||
|
|
||||||
|
div.ganttview-slide-container {
|
||||||
|
overflow: auto;
|
||||||
|
border-left: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid */
|
||||||
|
|
||||||
|
div.ganttview-grid-row-cell {
|
||||||
|
width: 20px;
|
||||||
|
height: 31px;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-grid-row-cell.ganttview-weekend {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-grid-row-cell.ganttview-today {
|
||||||
|
background-color: #F7F9FF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blocks */
|
||||||
|
|
||||||
|
div.ganttview-blocks {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-block-container {
|
||||||
|
height: 31px;
|
||||||
|
padding-top: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-block {
|
||||||
|
position: relative;
|
||||||
|
height: 25px;
|
||||||
|
background-color: #E5ECF9;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ganttview-block-text {
|
||||||
|
position: absolute;
|
||||||
|
height: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
padding: 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjustments for jQuery UI Styling */
|
||||||
|
|
||||||
|
div.ganttview-block div.ui-resizable-handle.ui-resizable-s {
|
||||||
|
bottom: -0;
|
||||||
|
}
|
396
addons/web/static/lib/jquery.ganttView/jquery.ganttView.js
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
/*
|
||||||
|
jQuery.ganttView v.0.8.8
|
||||||
|
Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com
|
||||||
|
MIT License Applies
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Options
|
||||||
|
-----------------
|
||||||
|
showWeekends: boolean
|
||||||
|
showToday: boolean
|
||||||
|
data: object
|
||||||
|
cellWidth: number
|
||||||
|
cellHeight: number
|
||||||
|
slideWidth: number
|
||||||
|
dataUrl: string
|
||||||
|
behavior: {
|
||||||
|
clickable: boolean,
|
||||||
|
draggable: boolean,
|
||||||
|
resizable: boolean,
|
||||||
|
onDblClick: function, // Modify by Flectra - Click becomes DblClick
|
||||||
|
onDrag: function,
|
||||||
|
onResize: function
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (jQuery) {
|
||||||
|
|
||||||
|
jQuery.fn.ganttView = function () {
|
||||||
|
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
|
||||||
|
if (args.length == 1 && typeof(args[0]) == "object") {
|
||||||
|
build.call(this, args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 2 && typeof(args[0]) == "string") {
|
||||||
|
handleMethod.call(this, args[0], args[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function build(options) {
|
||||||
|
|
||||||
|
var els = this;
|
||||||
|
var defaults = {
|
||||||
|
showWeekends: true,
|
||||||
|
showToday: true,
|
||||||
|
cellWidth: 21,
|
||||||
|
cellHeight: 31,
|
||||||
|
slideWidth: 400,
|
||||||
|
vHeaderWidth: 100,
|
||||||
|
behavior: {
|
||||||
|
clickable: true,
|
||||||
|
draggable: true,
|
||||||
|
resizable: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var opts = jQuery.extend(true, defaults, options);
|
||||||
|
|
||||||
|
if (opts.data) {
|
||||||
|
build();
|
||||||
|
} else if (opts.dataUrl) {
|
||||||
|
jQuery.getJSON(opts.dataUrl, function (data) { opts.data = data; build(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
|
||||||
|
var minDays = Math.floor((opts.slideWidth / opts.cellWidth) + 5);
|
||||||
|
var startEnd = DateUtils.getBoundaryDatesFromData(opts.data, minDays);
|
||||||
|
opts.start = startEnd[0];
|
||||||
|
opts.end = startEnd[1];
|
||||||
|
|
||||||
|
els.each(function () {
|
||||||
|
|
||||||
|
var container = jQuery(this);
|
||||||
|
var div = jQuery("<div>", { "class": "ganttview" });
|
||||||
|
new Chart(div, opts).render();
|
||||||
|
container.append(div);
|
||||||
|
|
||||||
|
var w = jQuery("div.ganttview-vtheader", container).outerWidth() +
|
||||||
|
jQuery("div.ganttview-slide-container", container).outerWidth();
|
||||||
|
container.css("width", (w + 2) + "px");
|
||||||
|
|
||||||
|
new Behavior(container, opts).apply();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMethod(method, value) {
|
||||||
|
|
||||||
|
if (method == "setSlideWidth") {
|
||||||
|
var div = $("div.ganttview", this);
|
||||||
|
div.each(function () {
|
||||||
|
var vtWidth = $("div.ganttview-vtheader", div).outerWidth();
|
||||||
|
$(div).width(vtWidth + value + 1);
|
||||||
|
$("div.ganttview-slide-container", this).width(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Chart = function(div, opts) {
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
addVtHeader(div, opts.data, opts.cellHeight);
|
||||||
|
|
||||||
|
var slideDiv = jQuery("<div>", {
|
||||||
|
"class": "ganttview-slide-container",
|
||||||
|
"css": { "width": opts.slideWidth + "px" }
|
||||||
|
});
|
||||||
|
|
||||||
|
dates = getDates(opts.start, opts.end);
|
||||||
|
addHzHeader(slideDiv, dates, opts.cellWidth);
|
||||||
|
addGrid(slideDiv, opts.data, dates, opts.cellWidth, opts.showWeekends, opts.showToday);
|
||||||
|
addBlockContainers(slideDiv, opts.data);
|
||||||
|
addBlocks(slideDiv, opts.data, opts.cellWidth, opts.start);
|
||||||
|
div.append(slideDiv);
|
||||||
|
applyLastClass(div.parent());
|
||||||
|
}
|
||||||
|
|
||||||
|
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
// Creates a 3 dimensional array [year][month][day] of every day
|
||||||
|
// between the given start and end dates
|
||||||
|
function getDates(start, end) {
|
||||||
|
var end = end.clone().addDays(6);
|
||||||
|
var dates = [];
|
||||||
|
dates[start.getFullYear()] = [];
|
||||||
|
dates[start.getFullYear()][start.getMonth()] = [start]
|
||||||
|
var last = start;
|
||||||
|
while (last.compareTo(end) == -1) {
|
||||||
|
var next = last.clone().addDays(1);
|
||||||
|
if (!dates[next.getFullYear()]) { dates[next.getFullYear()] = []; }
|
||||||
|
if (!dates[next.getFullYear()][next.getMonth()]) {
|
||||||
|
dates[next.getFullYear()][next.getMonth()] = [];
|
||||||
|
}
|
||||||
|
dates[next.getFullYear()][next.getMonth()].push(next);
|
||||||
|
last = next;
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVtHeader(div, data, cellHeight) {
|
||||||
|
var headerDiv = jQuery("<div>", { "class": "ganttview-vtheader" });
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var itemDiv = jQuery("<div>", { "class": "ganttview-vtheader-item" });
|
||||||
|
if ($.trim(data[i].name).length > 0)
|
||||||
|
itemDiv.append(jQuery("<div>", {
|
||||||
|
"class": "ganttview-vtheader-item-name",
|
||||||
|
"css": { "height": (data[i].series.length * cellHeight) + "px" }
|
||||||
|
}).append(data[i].name));
|
||||||
|
var seriesDiv = jQuery("<div>", { "class": "ganttview-vtheader-series" });
|
||||||
|
for (var j = 0; j < data[i].series.length; j++) {
|
||||||
|
seriesDiv.append(jQuery("<div>", { "class": "ganttview-vtheader-series-name" })
|
||||||
|
.append(data[i].series[j].name));
|
||||||
|
}
|
||||||
|
itemDiv.append(seriesDiv);
|
||||||
|
headerDiv.append(itemDiv);
|
||||||
|
}
|
||||||
|
div.append(headerDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHzHeader(div, dates, cellWidth) {
|
||||||
|
var headerDiv = jQuery("<div>", { "class": "ganttview-hzheader" });
|
||||||
|
var monthsDiv = jQuery("<div>", { "class": "ganttview-hzheader-months" });
|
||||||
|
var daysDiv = jQuery("<div>", { "class": "ganttview-hzheader-days" });
|
||||||
|
var totalW = 0;
|
||||||
|
for (var y in dates) {
|
||||||
|
for (var m in dates[y]) {
|
||||||
|
var w = dates[y][m].length * cellWidth;
|
||||||
|
totalW = totalW + w;
|
||||||
|
monthsDiv.append(jQuery("<div>", {
|
||||||
|
"class": "ganttview-hzheader-month",
|
||||||
|
"css": { "width": w + "px" }
|
||||||
|
}).append(monthNames[m] + "/" + y));
|
||||||
|
for (var d in dates[y][m]) {
|
||||||
|
daysDiv.append(jQuery("<div>", { "class": "ganttview-hzheader-day" })
|
||||||
|
.append(dates[y][m][d].getDate()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monthsDiv.css("width", totalW + "px");
|
||||||
|
daysDiv.css("width", totalW + "px");
|
||||||
|
headerDiv.append(monthsDiv).append(daysDiv);
|
||||||
|
div.append(headerDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGrid(div, data, dates, cellWidth, showWeekends, showToday) {
|
||||||
|
var gridDiv = jQuery("<div>", { "class": "ganttview-grid" });
|
||||||
|
var rowDiv = jQuery("<div>", { "class": "ganttview-grid-row" });
|
||||||
|
for (var y in dates) {
|
||||||
|
for (var m in dates[y]) {
|
||||||
|
for (var d in dates[y][m]) {
|
||||||
|
var cellDiv = jQuery("<div>", { "class": "ganttview-grid-row-cell" });
|
||||||
|
if (DateUtils.isWeekend(dates[y][m][d]) && showWeekends) {
|
||||||
|
cellDiv.addClass("ganttview-weekend");
|
||||||
|
}
|
||||||
|
if (DateUtils.isToday(dates[y][m][d]) && showToday) {
|
||||||
|
cellDiv.addClass("ganttview-today");
|
||||||
|
}
|
||||||
|
rowDiv.append(cellDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var w = jQuery("div.ganttview-grid-row-cell", rowDiv).length * cellWidth;
|
||||||
|
rowDiv.css("width", w + "px");
|
||||||
|
gridDiv.css("width", w + "px");
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
for (var j = 0; j < data[i].series.length; j++) {
|
||||||
|
gridDiv.append(rowDiv.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.append(gridDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBlockContainers(div, data) {
|
||||||
|
var blocksDiv = jQuery("<div>", { "class": "ganttview-blocks" });
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
for (var j = 0; j < data[i].series.length; j++) {
|
||||||
|
blocksDiv.append(jQuery("<div>", { "class": "ganttview-block-container" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.append(blocksDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBlocks(div, data, cellWidth, start) {
|
||||||
|
var rows = jQuery("div.ganttview-blocks div.ganttview-block-container", div);
|
||||||
|
var rowIdx = 0;
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
for (var j = 0; j < data[i].series.length; j++) {
|
||||||
|
var series = data[i].series[j];
|
||||||
|
var size = DateUtils.daysBetween(series.start, series.end) + 1;
|
||||||
|
var offset = DateUtils.daysBetween(start, series.start);
|
||||||
|
var block = jQuery("<div>", {
|
||||||
|
"class": "ganttview-block",
|
||||||
|
"title": series.name + ", " + size + " days \n"+ series.start +" to "+ series.end,
|
||||||
|
"css": {
|
||||||
|
"width": ((size * cellWidth) - 7) + "px",
|
||||||
|
"margin-left": ((offset * cellWidth) + 3) + "px"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addBlockData(block, data[i], series);
|
||||||
|
if (data[i].series[j].color) {
|
||||||
|
block.css("background-color", data[i].series[j].color);
|
||||||
|
}
|
||||||
|
block.append(jQuery("<div>", { "class": "ganttview-block-text" }).text(size));
|
||||||
|
jQuery(rows[rowIdx]).append(block);
|
||||||
|
rowIdx = rowIdx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBlockData(block, data, series) {
|
||||||
|
// This allows custom attributes to be added to the series data objects
|
||||||
|
// and makes them available to the 'data' argument of click, resize, and drag handlers
|
||||||
|
var blockData = { id: data.id, name: data.name };
|
||||||
|
jQuery.extend(blockData, series);
|
||||||
|
block.data("block-data", blockData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLastClass(div) {
|
||||||
|
jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child", div).addClass("last");
|
||||||
|
jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child", div).addClass("last");
|
||||||
|
jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child", div).addClass("last");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: render
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var Behavior = function (div, opts) {
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
|
||||||
|
if (opts.behavior.clickable) {
|
||||||
|
bindBlockClick(div, opts.behavior.onDblClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.behavior.resizable) {
|
||||||
|
bindBlockResize(div, opts.cellWidth, opts.start, opts.behavior.onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.behavior.draggable) {
|
||||||
|
bindBlockDrag(div, opts.cellWidth, opts.start, opts.behavior.onDrag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindBlockClick(div, callback) {
|
||||||
|
jQuery("div.ganttview-block", div).on("dblclick", function () {
|
||||||
|
if (callback) { callback(jQuery(this).data("block-data")); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindBlockResize(div, cellWidth, startDate, callback) {
|
||||||
|
jQuery("div.ganttview-block", div).resizable({
|
||||||
|
grid: cellWidth,
|
||||||
|
handles: "e,w",
|
||||||
|
stop: function () {
|
||||||
|
var block = jQuery(this);
|
||||||
|
updateDataAndPosition(div, block, cellWidth, startDate);
|
||||||
|
if (callback) { callback(block.data("block-data")); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindBlockDrag(div, cellWidth, startDate, callback) {
|
||||||
|
jQuery("div.ganttview-block", div).draggable({
|
||||||
|
axis: "x",
|
||||||
|
grid: [cellWidth, cellWidth],
|
||||||
|
stop: function () {
|
||||||
|
var block = jQuery(this);
|
||||||
|
updateDataAndPosition(div, block, cellWidth, startDate);
|
||||||
|
if (callback) { callback(block.data("block-data")); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDataAndPosition(div, block, cellWidth, startDate) {
|
||||||
|
var container = jQuery("div.ganttview-slide-container", div);
|
||||||
|
var scroll = container.scrollLeft();
|
||||||
|
var offset = block.offset().left - container.offset().left - 1 + scroll;
|
||||||
|
|
||||||
|
// Set new start date
|
||||||
|
var daysFromStart = Math.round(offset / cellWidth);
|
||||||
|
var newStart = startDate.clone().addDays(daysFromStart);
|
||||||
|
block.data("block-data").start = newStart;
|
||||||
|
|
||||||
|
// Set new end date
|
||||||
|
var width = block.outerWidth();
|
||||||
|
var numberOfDays = Math.round(width / cellWidth) - 1;
|
||||||
|
block.data("block-data").end = newStart.clone().addDays(numberOfDays);
|
||||||
|
jQuery("div.ganttview-block-text", block).text(numberOfDays + 1);
|
||||||
|
|
||||||
|
// Remove top and left properties to avoid incorrect block positioning,
|
||||||
|
// set position to relative to keep blocks relative to scrollbar when scrolling
|
||||||
|
block.css("top", "").css("left", "")
|
||||||
|
.css("position", "relative").css("margin-left", offset + "px");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apply: apply
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var ArrayUtils = {
|
||||||
|
|
||||||
|
contains: function (arr, obj) {
|
||||||
|
var has = false;
|
||||||
|
for (var i = 0; i < arr.length; i++) { if (arr[i] == obj) { has = true; } }
|
||||||
|
return has;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var DateUtils = {
|
||||||
|
|
||||||
|
daysBetween: function (start, end) {
|
||||||
|
if (!start || !end) { return 0; }
|
||||||
|
start = Date.parse(start); end = Date.parse(end);
|
||||||
|
if (start.getYear() == 1901 || end.getYear() == 8099) { return 0; }
|
||||||
|
var count = 0, date = start.clone();
|
||||||
|
while (date.compareTo(end) == -1) { count = count + 1; date.addDays(1); }
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
|
isWeekend: function (date) {
|
||||||
|
return date.getDay() % 6 == 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
isToday: function (date) {
|
||||||
|
return date.isToday();
|
||||||
|
},
|
||||||
|
|
||||||
|
getBoundaryDatesFromData: function (data, minDays) {
|
||||||
|
var minStart = new Date(); maxEnd = new Date();
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
for (var j = 0; j < data[i].series.length; j++) {
|
||||||
|
var start = Date.parse(data[i].series[j].start);
|
||||||
|
var end = Date.parse(data[i].series[j].end)
|
||||||
|
if (i == 0 && j == 0) { minStart = start; maxEnd = end; }
|
||||||
|
if (minStart.compareTo(start) == 1) { minStart = start; }
|
||||||
|
if (maxEnd.compareTo(end) == -1) { maxEnd = end; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insure that the width of the chart is at least the slide width to avoid empty
|
||||||
|
// whitespace to the right of the grid
|
||||||
|
if (DateUtils.daysBetween(minStart, maxEnd) < minDays) {
|
||||||
|
maxEnd = minStart.clone().addDays(minDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [minStart, maxEnd];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
25
addons/web/static/src/js/views/gantt/gantt_controller.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
flectra.define('web.GanttController', function (require) {
|
||||||
|
"use strict";
|
||||||
|
/*---------------------------------------------------------
|
||||||
|
* Flectra Gantt view
|
||||||
|
*---------------------------------------------------------*/
|
||||||
|
|
||||||
|
var AbstractController = require('web.AbstractController');
|
||||||
|
|
||||||
|
return AbstractController.extend({
|
||||||
|
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
|
||||||
|
updateRecord: '_onUpdateRecord',
|
||||||
|
}),
|
||||||
|
_onUpdateRecord: function (record) {
|
||||||
|
this._rpc({
|
||||||
|
model: this.model.modelName,
|
||||||
|
method: 'write',
|
||||||
|
args: [record.data.id, {
|
||||||
|
[this.model.data.arch['date_start']]: record.data.start.toString('yyyy-M-d'),
|
||||||
|
[this.model.data.arch['date_stop']]: record.data.end.toString('yyyy-M-d'),
|
||||||
|
}],
|
||||||
|
}).then(this.reload.bind(this));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
144
addons/web/static/src/js/views/gantt/gantt_model.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
flectra.define('web.GanttModel', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gantt model is responsible for fetching and processing data from the
|
||||||
|
* server. It basically just do a search_read and format/normalize data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var AbstractModel = require('web.AbstractModel');
|
||||||
|
|
||||||
|
return AbstractModel.extend({
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
init: function () {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
this.data = null;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string[]} params.groupedBy a list of valid field names
|
||||||
|
* @param {Object} params.context
|
||||||
|
* @param {string[]} params.domain
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
load: function (params) {
|
||||||
|
this.modelName = params.modelName;
|
||||||
|
this.data = {
|
||||||
|
records: [],
|
||||||
|
domain: params.domain,
|
||||||
|
context: params.context,
|
||||||
|
groupedBy: params.groupedBy || [],
|
||||||
|
arch: params.arch.attrs,
|
||||||
|
};
|
||||||
|
return this._loadData();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string[]} params.groupedBy a list of valid field names
|
||||||
|
* @param {Object} params.context
|
||||||
|
* @param {string[]} params.domain
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
reload: function (handle, params) {
|
||||||
|
if (params.domain) {
|
||||||
|
this.data.domain = params.domain;
|
||||||
|
}
|
||||||
|
if (params.context) {
|
||||||
|
this.data.context = params.context;
|
||||||
|
}
|
||||||
|
if (params.groupBy) {
|
||||||
|
this.data.groupedBy = params.groupBy;
|
||||||
|
}
|
||||||
|
return this._loadData();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
_loadData: function () {
|
||||||
|
var self = this;
|
||||||
|
return this._rpc({
|
||||||
|
model: this.modelName,
|
||||||
|
method: 'search_read',
|
||||||
|
context: this.data.context,
|
||||||
|
domain: this.data.domain,
|
||||||
|
})
|
||||||
|
.then(function (records) {
|
||||||
|
self.data.records = self._processData(records);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_processData: function (raw_datas) {
|
||||||
|
/**
|
||||||
|
* GroupBy is only supported till 1st level !
|
||||||
|
* @todo Flectra: Support Multi level GroupBy
|
||||||
|
*/
|
||||||
|
var self = this;
|
||||||
|
var ganttData = [];
|
||||||
|
if (self.data.groupedBy.length) {
|
||||||
|
_.each(raw_datas, function (raw_data) {
|
||||||
|
var grpByStr = raw_data[self.data.groupedBy[0]] ? raw_data[self.data.groupedBy[0]] : 'Undefined';
|
||||||
|
if (grpByStr && grpByStr instanceof Array) {
|
||||||
|
grpByStr = raw_data[self.data.groupedBy[0]] ? raw_data[self.data.groupedBy[0]][1] : 'Undefined';
|
||||||
|
}
|
||||||
|
var keyCheck = _.findKey(ganttData, {name: grpByStr});
|
||||||
|
if (!keyCheck) {
|
||||||
|
ganttData.push({
|
||||||
|
name: grpByStr,
|
||||||
|
series: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
keyCheck = _.findKey(ganttData, {name: grpByStr});
|
||||||
|
if (ganttData[keyCheck]) {
|
||||||
|
if (raw_data[self.data.arch['date_stop']]) {
|
||||||
|
ganttData[keyCheck].series.push({
|
||||||
|
id: raw_data['id'],
|
||||||
|
name: raw_data['display_name'],
|
||||||
|
start: raw_data[self.data.arch['date_start']].split(' ')[0],
|
||||||
|
end: raw_data[self.data.arch['date_stop']].split(' ')[0]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ganttData[keyCheck].series.push({
|
||||||
|
id: raw_data['id'],
|
||||||
|
name: raw_data['display_name'],
|
||||||
|
start: raw_data[self.data.arch['date_start']].split(' ')[0],
|
||||||
|
end: raw_data[self.data.arch['date_start']].split(' ')[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_.each(raw_datas, function (raw_data) {
|
||||||
|
if (raw_data[self.data.arch['date_stop']]) {
|
||||||
|
ganttData.push({
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: raw_data['id'],
|
||||||
|
name: raw_data['display_name'],
|
||||||
|
start: raw_data[self.data.arch['date_start']].split(' ')[0],
|
||||||
|
end: raw_data[self.data.arch['date_stop']].split(' ')[0]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ganttData.push({
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: raw_data['id'],
|
||||||
|
name: raw_data['display_name'],
|
||||||
|
start: raw_data[self.data.arch['date_start']].split(' ')[0],
|
||||||
|
end: raw_data[self.data.arch['date_start']].split(' ')[0]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ganttData;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
122
addons/web/static/src/js/views/gantt/gantt_renderer.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
flectra.define('web.GanttRenderer', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The graph renderer turns the data from the graph model into a nice looking
|
||||||
|
* svg chart. This code uses the nvd3 library.
|
||||||
|
*
|
||||||
|
* Note that we use a custom build for the nvd3, with only the model we actually
|
||||||
|
* use.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var core = require('web.core');
|
||||||
|
var AbstractRenderer = require('web.AbstractRenderer');
|
||||||
|
var Dialog = require('web.Dialog');
|
||||||
|
|
||||||
|
var _t = core._t;
|
||||||
|
var QWeb = core.qweb;
|
||||||
|
|
||||||
|
return AbstractRenderer.extend({
|
||||||
|
template: "GanttView",
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Widget} parent
|
||||||
|
* @param {Object} state
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {boolean} params.stacked
|
||||||
|
*/
|
||||||
|
init: function (parent, state, params) {
|
||||||
|
this.parent = parent;
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the chart.
|
||||||
|
*
|
||||||
|
* Note that This method is synchronous, but the actual rendering is done
|
||||||
|
* asynchronously (in a setTimeout). The reason for that is that nvd3/d3
|
||||||
|
* needs to be in the DOM to correctly render itself. So, we trick Flectra by
|
||||||
|
* returning immediately, then wait a tiny interval before actually
|
||||||
|
* displaying the data.
|
||||||
|
*
|
||||||
|
* @returns {Deferred} The _super deferred is actually resolved immediately
|
||||||
|
*/
|
||||||
|
_render: function () {
|
||||||
|
this.data = this.parent.active_view.controller.model.data;
|
||||||
|
this._loadGanttView();
|
||||||
|
return $.when();
|
||||||
|
},
|
||||||
|
_loadGanttView: function () {
|
||||||
|
var self = this;
|
||||||
|
this.$el.empty().ganttView({
|
||||||
|
data: self.data.records,
|
||||||
|
slideWidth: 'auto',
|
||||||
|
cellWidth: 20,
|
||||||
|
behavior: {
|
||||||
|
onDblClick: function (data) {
|
||||||
|
var dialog = new Dialog(self, {
|
||||||
|
title: _t(data.name),
|
||||||
|
$content: $(QWeb.render('GanttViewWizard')),
|
||||||
|
size: 'small',
|
||||||
|
buttons: [
|
||||||
|
{text: _t("Save"), classes: 'btn-success', click: _.bind(_callSave, self)},
|
||||||
|
{text: _t("Cancel"), classes: 'btn-danger', close: true}
|
||||||
|
]
|
||||||
|
}).open();
|
||||||
|
|
||||||
|
dialog.opened().then(function () {
|
||||||
|
var datepickers_options = {
|
||||||
|
keepOpen: true,
|
||||||
|
minDate: moment({y: 1900}),
|
||||||
|
maxDate: moment().add(200, "y"),
|
||||||
|
calendarWeeks: true,
|
||||||
|
icons: {
|
||||||
|
time: 'fa fa-clock-o',
|
||||||
|
date: 'fa fa-calendar',
|
||||||
|
next: 'fa fa-chevron-right',
|
||||||
|
previous: 'fa fa-chevron-left',
|
||||||
|
up: 'fa fa-chevron-up',
|
||||||
|
down: 'fa fa-chevron-down',
|
||||||
|
},
|
||||||
|
locale: moment.locale(),
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
ignoreReadonly: true
|
||||||
|
};
|
||||||
|
dialog.$el.find('input#start_date').val(data.start);
|
||||||
|
dialog.$el.find('input#end_date').val(data.end);
|
||||||
|
dialog.$el.find('input#start_date, input#end_date').datetimepicker(datepickers_options);
|
||||||
|
});
|
||||||
|
|
||||||
|
function _callSave(event) {
|
||||||
|
var newData = {
|
||||||
|
start: dialog.$el.find('input#start_date').val().toString(),
|
||||||
|
end: dialog.$el.find('input#end_date').val().toString(),
|
||||||
|
id: data.id
|
||||||
|
};
|
||||||
|
if (data.start !== newData.start || data.end !== newData.end) {
|
||||||
|
var start_data = new Date(dialog.$el.find('input#start_date').val().toString()).getTime();
|
||||||
|
var end_data = new Date(dialog.$el.find('input#end_date').val().toString()).getTime();
|
||||||
|
if(start_data <= end_data){
|
||||||
|
self.trigger_up('updateRecord', newData);
|
||||||
|
dialog.close();
|
||||||
|
}else {
|
||||||
|
self.do_warn(_t("Warning"), _t("Start date should be less than or equal to End date"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResize: function (data) {
|
||||||
|
self.trigger_up('updateRecord', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDrag: function (data) {
|
||||||
|
self.trigger_up('updateRecord', data);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$el.removeAttr('style');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
32
addons/web/static/src/js/views/gantt/gantt_view.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
flectra.define('web.GanttView', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var AbstractView = require('web.AbstractView');
|
||||||
|
var core = require('web.core');
|
||||||
|
var GanttModel = require('web.GanttModel');
|
||||||
|
var GanttRenderer = require('web.GanttRenderer');
|
||||||
|
var Controller = require('web.GanttController');
|
||||||
|
|
||||||
|
var _lt = core._lt;
|
||||||
|
|
||||||
|
var GanttView = AbstractView.extend({
|
||||||
|
display_name: _lt('Gantt'),
|
||||||
|
icon: 'fa-tasks',
|
||||||
|
config: {
|
||||||
|
Model: GanttModel,
|
||||||
|
Controller: Controller,
|
||||||
|
Renderer: GanttRenderer,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
init: function(viewInfo) {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
var arch = viewInfo.arch;
|
||||||
|
this.loadParams.arch = arch;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return GanttView;
|
||||||
|
|
||||||
|
});
|
@ -31,6 +31,7 @@ var KanbanView = require('web.KanbanView');
|
|||||||
var ListView = require('web.ListView');
|
var ListView = require('web.ListView');
|
||||||
var PivotView = require('web.PivotView');
|
var PivotView = require('web.PivotView');
|
||||||
var CalendarView = require('web.CalendarView');
|
var CalendarView = require('web.CalendarView');
|
||||||
|
var GanttView = require('web.GanttView');
|
||||||
var view_registry = require('web.view_registry');
|
var view_registry = require('web.view_registry');
|
||||||
|
|
||||||
view_registry
|
view_registry
|
||||||
@ -39,6 +40,7 @@ view_registry
|
|||||||
.add('kanban', KanbanView)
|
.add('kanban', KanbanView)
|
||||||
.add('graph', GraphView)
|
.add('graph', GraphView)
|
||||||
.add('pivot', PivotView)
|
.add('pivot', PivotView)
|
||||||
.add('calendar', CalendarView);
|
.add('calendar', CalendarView)
|
||||||
|
.add('gantt', GanttView);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -528,6 +528,39 @@
|
|||||||
<p><t t-esc="description"/></p>
|
<p><t t-esc="description"/></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<t t-name="GanttView">
|
||||||
|
<div class="o_gantt_view_container"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="GanttViewWizard">
|
||||||
|
<table class="o_group o_inner_group" style="width: 100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="o_td_label" style="padding: 10px 10px 10px 20px;">
|
||||||
|
<label class="o_form_label" for="start_date">Start Date</label>
|
||||||
|
</td>
|
||||||
|
<td style="width: 65%; padding: 10px 10px 10px 20px;">
|
||||||
|
<div class="o_datepicker o_field_date o_field_widget" name="date_from">
|
||||||
|
<input class="o_datepicker_input o_input"
|
||||||
|
type="text" name="date_from" id="start_date"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="o_td_label" style="padding: 10px 10px 10px 20px;">
|
||||||
|
<label class="o_form_label" for="end_date">End Date</label>
|
||||||
|
</td>
|
||||||
|
<td style="width: 65%; padding: 10px 10px 10px 20px;">
|
||||||
|
<div class="o_datepicker o_field_date o_field_widget" name="date_to">
|
||||||
|
<input class="o_datepicker_input o_input"
|
||||||
|
type="text" name="date_to" id="end_date"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</t>
|
||||||
|
|
||||||
<div t-name="PivotView" class="o_pivot">
|
<div t-name="PivotView" class="o_pivot">
|
||||||
<div class="o_field_selection"/>
|
<div class="o_field_selection"/>
|
||||||
</div>
|
</div>
|
||||||
@ -1332,7 +1365,6 @@
|
|||||||
<li><a href="#" data-menu="support">Support</a></li>
|
<li><a href="#" data-menu="support">Support</a></li>
|
||||||
<li class="divider"/>
|
<li class="divider"/>
|
||||||
<li><a href="#" data-menu="settings">Preferences</a></li>
|
<li><a href="#" data-menu="settings">Preferences</a></li>
|
||||||
<li><a href="#" data-menu="account">My Flectra.com account</a></li>
|
|
||||||
<li><a href="#" data-menu="logout">Log out</a></li>
|
<li><a href="#" data-menu="logout">Log out</a></li>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
@ -236,6 +236,18 @@
|
|||||||
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
|
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
|
||||||
<script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
|
<script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
|
||||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/report_backend.less"/>
|
<link rel="stylesheet" type="text/less" href="/web/static/src/less/report_backend.less"/>
|
||||||
|
|
||||||
|
<!-- @Flectra: Gantt View Assets ::: Start -->
|
||||||
|
<script type="text/javascript" src="/web/static/lib/jquery.ganttView/date.js"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ganttView/jquery.ganttView.css"/>
|
||||||
|
<script type="text/javascript" src="/web/static/lib/jquery.ganttView/jquery.ganttView.js"/>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_model.js"/>
|
||||||
|
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_controller.js"/>
|
||||||
|
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_renderer.js"/>
|
||||||
|
<script type="text/javascript" src="/web/static/src/js/views/gantt/gantt_view.js"/>
|
||||||
|
<!-- @Flectra: Gantt View Assets ::: End -->
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="web.assets_frontend" name="Website Assets">
|
<template id="web.assets_frontend" name="Website Assets">
|
||||||
@ -370,9 +382,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_sub_menu_footer">
|
|
||||||
Powered by <a href="http://www.flectra.com" target="_blank"><span>Flectra</span></a>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template id="web.menu_secondary_submenu">
|
<template id="web.menu_secondary_submenu">
|
||||||
<ul t-if="menu['children']" class="oe_secondary_submenu nav nav-pills nav-stacked">
|
<ul t-if="menu['children']" class="oe_secondary_submenu nav nav-pills nav-stacked">
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -160,7 +160,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div id="settings" class="tab-pane active text-muted text-center o_web_settings_dashboard_compact_subtitle">
|
<div id="settings" class="tab-pane active text-muted text-center o_web_settings_dashboard_compact_subtitle">
|
||||||
<small>Copyright © 2004-2016 <a target="_blank" href="https://www.flectra.com" style="text-decoration: underline;">Flectra S.A.</a> <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html" style="text-decoration: underline;">GNU LGPL Licensed</a></small>
|
<small>Copyright © 2004-Today <a target="_blank" href="https://www.flectrahq.com" style="text-decoration: underline;"> FlectraHQ</a> , <a href="https://www.odoo.com" style="text-decoration: underline;" target="_blank">Odoo S.A.</a> , <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html" style="text-decoration: underline;">GNU LGPL Licensed</a></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
<div class="mt32">
|
<div class="mt32">
|
||||||
<address>
|
<address>
|
||||||
For the Flectra Team,<br/>
|
For the Flectra Team,<br/>
|
||||||
Fabien Pinckaers, Founder
|
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.3 KiB |