# HG changeset patch
# User bsw/jbe
# Date 1531656449 -7200
# Node ID 32cc544d5a5b8604f3ed1dd9444b0e8d038bc2ef
# Parent 7ea154c9238a119216519b1ccf1b3233ebf43769
Cumulative patch for upcoming frontend version 4
diff -r 7ea154c9238a -r 32cc544d5a5b LICENSE
--- a/LICENSE Thu Jun 23 03:30:57 2016 +0200
+++ b/LICENSE Sun Jul 15 14:07:29 2018 +0200
@@ -23,14 +23,14 @@
3rd party license information:
------------------------------------------------------------------------------
+=============================================================================
Some of the icons used in LiquidFeedback (except national flags) are from
Silk icon set 1.3 by Mark James. [ http://www.famfamfam.com/lab/icons/silk/ ]
His work is licensed under a Creative Commons Attribution 2.5 License.
[ http://creativecommons.org/licenses/by/2.5/ ]
------------------------------------------------------------------------------
+=============================================================================
The emoticons are taken from the following web pages:
@@ -44,7 +44,7 @@
unless such conditions are required by law."
The orange and red smiley are modified versions of Face-sad.
------------------------------------------------------------------------------
+=============================================================================
The "jquery" library is licensed as follows:
@@ -86,7 +86,7 @@
own licenses; we recommend you read them, as their terms may differ from
the terms above.
------------------------------------------------------------------------------
+=============================================================================
"LESS - Leaner CSS" is licensed as follows:
@@ -269,7 +269,7 @@
END OF TERMS AND CONDITIONS
------------------------------------------------------------------------------
+=============================================================================
The collection of LDAP error codes (mldap_errorcodes[]) has been derived
from the file ldap.h that is part of OpenLDAP Software. OpenLDAP's license
@@ -348,5 +348,296 @@
End of OpenLDAP's license information
+=============================================================================
+
+"wysihtml" is licensed as follows:
+
+The MIT License (MIT)
+
+Copyright (C) 2012-2016 XING AG, Voog and contributors
+
+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.
+
+=============================================================================
+
+The Roboto font family is licensed as follows:
+
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of this License; and
+You must cause any modified files to carry prominent notices stating that You changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+=============================================================================
+
+The "mdl" library is licensed as follows:
+
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2015 Google Inc
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ All code in any directories or sub-directories that end with *.html or
+ *.css is licensed under the Creative Commons Attribution International
+ 4.0 License, which full text can be found here:
+ https://creativecommons.org/licenses/by/4.0/legalcode.
+
+ As an exception to this license, all html or css that is generated by
+ the software at the direction of the user is copyright the user. The
+ user has full ownership and control over such content, including
+ whether and how they wish to license it.
+
+=============================================================================
END OF LICENSE FILE
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_filter/20_session.lua
--- a/app/main/_filter/20_session.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_filter/20_session.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,4 +1,4 @@
-local cookie = request.get_cookie{ name = "liquid_feedback_session" }
+local cookie = request.get_cookie{ name = config.cookie_name or "liquid_feedback_session" }
if cookie then
app.session = Session:by_ident(cookie)
@@ -6,12 +6,12 @@
if not app.session then
app.session = Session:new()
request.set_cookie{
- name = "liquid_feedback_session",
+ name = config.cookie_name or "liquid_feedback_session",
value = app.session.ident
}
end
-request.set_csrf_secret(app.session.additional_secret)
+request.set_csrf_secret(app.session:additional_secret_for("csrf"))
locale.set{ lang = app.session.lang or config.default_lang or "en" }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_filter/21_auth.lua
--- a/app/main/_filter/21_auth.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_filter/21_auth.lua Sun Jul 15 14:07:29 2018 +0200
@@ -2,26 +2,59 @@
local view = request.get_view()
local action = request.get_action()
-local auth_needed = not (
- module == 'index'
- and (
-view == "login"
- or action == "login"
- or view == "register"
- or action == "register"
- or action == "cancel_register"
- or view == "about"
- or view == "reset_password"
- or action == "reset_password"
- or view == "send_login"
- or action == "send_login"
- or view == "confirm_notify_email"
- or action == "confirm_notify_email"
- or view == "menu"
- or action == "set_lang"
- or view == "404"
- )
-)
+local auth_needed = true
+
+if module == 'index' and (
+ view == 'index'
+ or view == "login"
+ or action == "login"
+ or view == "register"
+ or action == "register"
+ or action == "cancel_register"
+ or view == "about"
+ or view == "reset_password"
+ or action == "reset_password"
+ or view == "send_login"
+ or action == "send_login"
+ or view == "confirm_notify_email"
+ or action == "confirm_notify_email"
+ or view == "menu"
+ or action == "set_lang"
+ or view == "403"
+ or view == "404"
+ or view == "405"
+) then
+ auth_needed = false
+end
+
+if module == "registration" then
+ auth_needed = false
+end
+
+if module == "style" then
+ auth_needed = false
+end
+
+if module == "help" then
+ auth_needed = false
+end
+
+if module == "oauth2" and (
+ view == "validate"
+ or view == "token"
+ or view == "session"
+ or view == "register"
+) then
+ auth_needed = false
+end
+
+if module == "oauth2_client" then
+ auth_needed = false
+end
+
+if module == "api" then
+ auth_needed = false
+end
if app.session:has_access("anonymous") then
@@ -41,6 +74,7 @@
or module == "index" and view == "search"
or module == "index" and view == "usage_terms"
or module == "help" and view == "introduction"
+ or module == "style"
then
auth_needed = false
end
@@ -94,11 +128,21 @@
if auth_needed and app.session.member == nil then
trace.debug("Not authenticated yet.")
+ local params = json.object()
+ for key, val in pairs(request.get_param_strings()) do
+ if type(val) == "string" then
+ params[key] = val
+ else
+ -- shouldn't happen
+ error("array type params not implemented")
+ end
+ end
request.redirect{
module = 'index', view = 'login', params = {
redirect_module = module,
redirect_view = view,
- redirect_id = param.get_id()
+ redirect_id = param.get_id(),
+ redirect_params = params
}
}
elseif auth_needed and app.session.member.locked then
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_filter_view/30_navigation.lua
--- a/app/main/_filter_view/30_navigation.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_filter_view/30_navigation.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,78 +1,118 @@
-slot.select ( 'instance_name', function ()
- slot.put(" @ ")
- slot.put ( encode.html ( config.instance_name ) )
-end)
-
-
-slot.select ( 'navigation_right', function ()
+execute.inner()
- if app.session:has_access ("anonymous") and not (app.session.needs_delegation_check) then
-
- ui.form {
- attr = { class = "inline search" },
- method = "get",
- module = "index", view = "search",
- content = function ()
-
- ui.field.text {
- attr = { placeholder = _"search" },
- name = "q"
- }
-
- end
- }
-
- ui.link {
- attr = { class = "searchLink" },
- module = "index", view = "search", content = function ()
- ui.image { static = "icons/16/magnifier.png" }
- end
- }
-
+local for_meta_navigation = false
+if config.meta_navigation_items_func and config.meta_navigation_html_func then
+ for_meta_navigation =
+ request.get_module() == "index" and (
+ request.get_view() == "login"
+ or request.get_view() == "register"
+ or request.get_view() == "reset_password"
+ or request.get_view() == "send_login"
+ )
+ or (request.get_module() == "registration")
+ or (request.get_module() == "member" and request.get_view() == "show" and param.get_id() == app.session.member_id)
+ or (request.get_module() == "member" and request.get_view() == "history" and param.get_id() == app.session.member_id)
+ or (request.get_module() == "member" and (
+ string.match(request.get_view(), "^settings")
+ or string.match(request.get_view(), "^edit")
+ ))
+ local items = config.meta_navigation_items_func(app.session.member, for_meta_navigation and "login" or "LiquidFeedback")
+ local meta_navigation = config.meta_navigation_html_func(items)
+ slot.put_into("meta_navigation", meta_navigation)
+ local meta_navigation_style = config.meta_navigation_style_func(items)
+ slot.put_into("meta_navigation_style", meta_navigation_style)
+ if config.meta_navigation_extra_style_func then
+ local meta_navigation_extra_style = config.meta_navigation_extra_style_func(items)
+ slot.put_into("meta_navigation_style", meta_navigation_extra_style)
end
-
- if app.session.member == nil then
-
- slot.put ( " " )
-
- ui.link {
- text = _"Login",
- module = 'index',
- view = 'login',
- params = {
- redirect_module = request.get_module(),
- redirect_view = request.get_view(),
- redirect_id = param.get_id()
- }
- }
-
- slot.put ( " " )
- end
-
- if app.session.member == nil and not config.registration_disabled then
-
- ui.link {
- text = _"Registration",
- module = 'index',
- view = 'register'
- }
+ local meta_navigation_script = config.meta_navigation_script_func(items)
+ slot.put_into("script", meta_navigation_script)
+end
+
+if not config.meta_navigation_items_func or not config.meta_navigation_html_func then
+ slot.select ( 'header_bar', function ()
+ ui.tag{ tag = "header", attr = { class = "mdl-layout__header mdl-layout__header--seamed" }, content = function()
+ ui.container{ attr = { class = "mdl-layout__header-row" }, content = function()
+ ui.link{ module = "index", view = "index", attr = { class = "mdl-layout-title" }, content = "LiquidFeedback" }
+ ui.tag{ attr = { class = "mdl-layout-spacer" }, content = "" }
- end
-
-
- if app.session.member then
-
- slot.put ( " " )
-
- ui.tag { attr = { id = "member_menu" }, content = function()
- util.micro_avatar(app.session.member)
+ if app.session:has_access ("anonymous") then
+ ui.form { attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield--floating-label mdl-textfield--align-right" }, method = "get", module = "index", view = "search", content = function ()
+ ui.tag{ tag = "label", attr = { class = "mdl-button mdl-js-button mdl-button--icon", ["for"] = "fixed-header-drawer-exp" }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "search" }
+ end }
+ ui.container{ attr = { class = "mdl-textfield__expandable-holder" }, content = function()
+ ui.tag{ tag = "input", attr = { class = "mdl-textfield__input", type = "text", name = "q", id = "fixed-header-drawer-exp" }, content = "" }
+ end }
+ end }
+ end
+
+ if app.session.member == nil and not (
+ request.get_module() == "index" and (request.get_view() == "login" or request.get_view() == "reset_password" or request.get_view() == "send_login")
+ ) and not config.meta_navigation_html_func then
+ local redirect_params = json.object()
+ for key, val in pairs(request.get_param_strings()) do
+ if type(val) == "string" then
+ redirect_params[key] = val
+ else
+ -- shouldn't happen
+ error("array type params not implemented")
+ end
+ end
+ ui.tag{ tag = "nav", attr = { class = "mdl-navigation" }, content = function()
+ local link = {
+ content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "exit_to_app" }
+ slot.put(" ")
+ ui.tag{ attr = { class = "mdl-layout--large-screen-only" }, content = function()
+ ui.tag{ content = _"Login" }
+ end }
+ end,
+ text = _"Login",
+ attr = { class = "mdl-navigation__link" }
+ }
+ if config.login and config.login.method == "oauth2" then
+ link.module = "oauth2_client"
+ link.view = "redirect"
+ link.params = { provider = config.login.provider }
+ else
+ link.module = 'index'
+ link.view = 'login'
+ link.params = {
+ redirect_module = request.get_module(),
+ redirect_view = request.get_view(),
+ redirect_id = param.get_id(),
+ redirect_params = redirect_params
+ }
+ end
+ ui.link(link)
+ end }
+ end
+
+ if app.session.member and not (
+ config.meta_navigation_items_func and config.meta_navigation_html_func
+ ) then
+ ui.tag{ tag = "nav", attr = { class = "mdl-navigation" }, content = function()
+ ui.tag{ tag = "span", module = "member", view = "show", id = app.session.member.id, attr = { id = "lf-member-menu", class = "mdl-navigation__link" }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "person" }
+ ui.tag{ attr = { class = "mdl-layout--large-screen-only" }, content = function()
+ ui.tag{ content = app.session.member.name }
+ end }
+ end }
+
+ ui.tag { tag = "ul", attr = { class = "mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect", ["for"] = "lf-member-menu" }, content = function()
+ execute.view{ module = "member", view = "_menu", params = { item_class = "mdl-menu__item", link_class = "mdl-menu__link" } }
+ end }
+ end }
+
+ end -- if app.session.member
+ end }
end }
-
- end -- if app.session.member
-
-end)
+ end)
+end
-- show notifications about things the user should take care of
+--[[
if app.session.member then
execute.view{
module = "index", view = "_sidebar_notifications", params = {
@@ -80,34 +120,48 @@
}
}
end
+--]]
slot.select ("footer", function ()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ content = _"Quick guide", module = "help", view = "introduction" }
+ end }
if app.session.member_id and app.session.member.admin then
- ui.link {
- text = _"System settings",
- module = 'admin',
- view = 'index'
- }
- slot.put(" · ")
+ ui.tag{ tag = "li", content = function()
+ if config.admin_link then
+ ui.link(config.admin_link)
+ else
+ ui.link{ content = _"System settings", module = "admin", view = "index" }
+ end
+ end }
end
- ui.link{
- text = _"About site",
- module = 'index',
- view = 'about'
- }
- if config.use_terms then
- slot.put(" · ")
+ ui.tag{ tag = "li", content = function()
ui.link{
- text = _"Use terms",
+ text = _"About site",
module = 'index',
- view = 'usage_terms'
+ view = 'about'
}
+ end }
+ if not config.extra_footer_func then
+ if config.use_terms and app.session.member then
+ ui.tag{ tag = "li", content = function()
+ ui.link{
+ text = _"Use terms",
+ module = 'index',
+ view = 'usage_terms'
+ }
+ end }
+ end
end
- slot.put(" · ")
- ui.link{
- text = _"LiquidFeedback",
- external = "http://www.liquidfeedback.org/"
- }
+ if config.extra_footer_func then
+ config.extra_footer_func()
+ end
+ ui.tag{ tag = "li", content = function()
+ ui.link{
+ text = _"LiquidFeedback",
+ external = "http://www.liquidfeedback.org/"
+ }
+ end }
end)
if not config.enable_debug_trace then
@@ -117,4 +171,19 @@
end
-execute.inner()
+
+if app.current_initiative then
+ app.current_issue = app.current_initiative.issue
+end
+
+if app.current_issue then
+ app.current_area = app.current_issue.area
+end
+
+if app.current_area then
+ app.current_unit = app.current_area.unit
+end
+
+if not for_meta_navigation then
+ execute.view{ module = "index", view = "_drawer" }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_filter_view/34_stylesheet.lua
--- a/app/main/_filter_view/34_stylesheet.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_filter_view/34_stylesheet.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,16 +1,3 @@
-local value
-if app.session.member then
- local setting_key = "liquidfeedback_frontend_stylesheet_url"
- local setting = Setting:by_pk(app.session.member.id, setting_key)
- value = setting and setting.value
-end
-
-if value then
- slot.put_into("stylesheet_url", value)
-else
- slot.put_into("stylesheet_url", request.get_relative_baseurl() .. "static/lf3.css")
-end
-
execute.inner()
if config.footer_html then
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_layout/default.html
--- a/app/main/_layout/default.html Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_layout/default.html Sun Jul 15 14:07:29 2018 +0200
@@ -1,276 +1,759 @@
-
-
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/_prefork/10_init.lua
--- a/app/main/_prefork/10_init.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/_prefork/10_init.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,4 +1,4 @@
-config.app_version = "3.2.1"
+config.app_version = "4.0.0-pre"
-- TODO abstraction
-- get record by id
@@ -64,6 +64,61 @@
config.ldap = {}
end
+if config.oauth2 then
+ local scopes = {
+ { scope = "authentication", name = { de = "Identität feststellen (nur Screen-Name)", en = "Determine identity (screen name only)" } },
+ { scope = "identification", name = { de = "Identität feststellen", en = "Determine identity" } },
+ { scope = "notify_email", name = { de = "E-Mail-Adresse für Benachrichtigungen verwenden", en = "Use email address for notifications" } },
+ { scope = "read_contents", name = { de = "Inhalte lesen", en = "Read content" } },
+ { scope = "read_authors", name = { de = "Autorennamen lesen", en = "Read author names" } },
+ { scope = "read_ratings", name = { de = "Bewertungen lesen", en = "Read ratings" } },
+ { scope = "read_identities", name = { de = "Identitäten lesen", en = "Read identities" } },
+ { scope = "read_profiles", name = { de = "Profile lesen", en = "Read profiles" } },
+ { scope = "post", name = { de = "Neue Inhalte veröffentlichen", en = "Post new content" } },
+ { scope = "rate", name = { de = "Bewertungen vornehmen", en = "Do ratings" } },
+ { scope = "vote", name = { de = "Abstimmen", en = "Vote" } },
+ { scope = "delegate", name = { de = "Delegieren", en = "Delegate" } },
+ { scope = "profile", name = { de = "Eigenes Profil lesen", en = "Read your profile" } },
+ { scope = "settings", name = { de = "Einstellungen einsehen", en = "Read your settings" } },
+ { scope = "update_name", name = { de = "Screen-Namen ändern", en = "Update screen name" } },
+ { scope = "update_notify_email", name = { de = "E-Mail-Adresse für Benachrichtigungen ändern", en = "Update notify email address" } },
+ { scope = "update_profile", name = { de = "Profil bearbeiten", en = "Update your profile" } },
+ { scope = "update_settings", name = { de = "Benutzereinstellungen ändern", en = "Update your settings" } }
+ }
+ local s = config.oauth2.available_scopes or {}
+ for i, scope in ipairs(scopes) do
+ s[#s+1] = scope
+ end
+ config.oauth2.available_scopes = s
+ if not config.oauth2.endpoint_magic then
+ config.oauth2.endpoint_magic = "liquidfeedback_client/redirection_endpoint"
+ end
+ if not config.oauth2.manifest_magic then
+ config.oauth2.manifest_magic = "liquidfeedback_client/manifest"
+ end
+ if not config.oauth2.host_func then
+ config.oauth2.host_func = function(domain) return extos.pfilter(nil, "host", "-t", "TXT", domain) end
+ end
+ if not config.oauth2.authorization_code_lifetime then
+ config.oauth2.authorization_code_lifetime = 5 * 60
+ end
+ if not config.oauth2.refresh_token_lifetime then
+ config.oauth2.refresh_token_lifetime = 60 * 60 * 24 * 30 * 3
+ end
+ if not config.oauth2.refresh_pause then
+ config.oauth2.refresh_pause = 60
+ end
+ if not config.oauth2.refresh_grace_period then
+ config.oauth2.refresh_grace_period = 60
+ end
+ if not config.oauth2.access_token_lifetime then
+ config.oauth2.access_token_lifetime = 60 * 60
+ end
+ if not config.oauth2.dynamic_registration_lifetime then
+ config.oauth2.dynamic_registration_lifetime = 60 * 60 * 24
+ end
+end
+
if not config.database then
config.database = { engine='postgresql', dbname='liquid_feedback' }
end
@@ -135,10 +190,22 @@
request.set_absolute_baseurl(config.absolute_base_url)
+-- TODO remove style cache
+
listen(listen_options)
listen{
{
+ proto = "main",
+ name = "process_event_stream",
+ handler = function(poll)
+ Event:process_stream(poll)
+ end
+ }
+}
+
+listen{
+ {
proto = "interval",
name = "send_pending_notifications",
delay = 5,
@@ -149,11 +216,6 @@
end
end
while true do
- if not Event:send_next_notification() then
- break
- end
- end
- while true do
if not InitiativeForNotification:notify_next_member() then
break
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/_action/area_update.lua
--- a/app/main/admin/_action/area_update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/_action/area_update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,6 +1,6 @@
local area = Area:by_id(param.get_id()) or Area:new()
-param.update(area, "unit_id", "name", "description", "external_reference", "active")
+param.update(area, "unit_id", "name", "description", "external_reference", "quorum_standard", "quorum_issues", "quorum_time", "quorum_exponent", "quorum_factor", "active")
if #area.name < 1 then
slot.select("error", function()
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/_action/member_update.lua
--- a/app/main/admin/_action/member_update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/_action/member_update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -2,7 +2,21 @@
local member = Member:by_id(id) or Member:new()
-param.update(member, "identification", "notify_email", "admin")
+param.update(member, "identification", "admin")
+
+local notify_email = param.get("notify_email")
+if notify_email == "" then
+ notify_email = nil
+end
+
+member.notify_email = notify_email
+
+local notify_email_unconfirmed = param.get("notify_email_unconfirmed")
+if notify_email_unconfirmed == "" then
+ notify_email_unconfirmed = nil
+end
+
+member.notify_email_unconfirmed = notify_email_unconfirmed
local locked = param.get("locked", atom.boolean)
if locked ~= nil then
@@ -40,10 +54,18 @@
local privilege = Privilege:new()
privilege.member_id = member.id
privilege.unit_id = config.single_unit_id
+ privilege.initiative_right = true
privilege.voting_right = true
privilege:save()
end
+if not id then
+ local profile = MemberProfile:new()
+ profile.member_id = member.id
+ profile.profile = json.object()
+ profile:save()
+end
+
local units = Unit:new_selector()
:add_field("privilege.member_id NOTNULL", "privilege_exists")
:add_field("privilege.voting_right", "voting_right")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/_action/policy_update.lua
--- a/app/main/admin/_action/policy_update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/_action/policy_update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -4,8 +4,8 @@
policy,
"index", "name", "description", "active",
"min_admission_time", "max_admission_time", "discussion_time", "verification_time", "voting_time",
- "issue_quorum_num", "issue_quorum_den",
- "initiative_quorum_num", "initiative_quorum_den",
+ "issue_quorum", "issue_quorum_num", "issue_quorum_den",
+ "initiative_quorum", "initiative_quorum_num", "initiative_quorum_den",
"direct_majority_num", "direct_majority_den", "direct_majority_strict", "direct_majority_positive", "direct_majority_non_negative",
"indirect_majority_num", "indirect_majority_den", "indirect_majority_strict", "indirect_majority_positive", "indirect_majority_non_negative",
"no_reverse_beat_path", "no_multistage_majority", "polling"
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/_filter/90_admin.lua
--- a/app/main/admin/_filter/90_admin.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/_filter/90_admin.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,5 @@
if not app.session.member.admin then
- error('access denied')
+ return execute.view { module = "index", view = "403" }
end
if config.admin_logger then
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/area_show.lua
--- a/app/main/admin/area_show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/area_show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -63,6 +63,11 @@
connecting_records = area.allowed_policies or {},
foreign_reference = "id",
}
+ ui.field.text{ label = _"Admission quorum standard", name = "quorum_standard", value = hint and 10 or nil }
+ ui.field.text{ label = _"Admission quorum issues", name = "quorum_issues", value = hint and 10 or nil }
+ ui.field.text{ label = _"Admission quorum time", name = "quorum_time", value = hint and "60 days" or nil }
+ ui.field.text{ label = _"Admission quorum exponent", name = "quorum_exponent", value = hint and 0.5 or nil }
+ ui.field.text{ label = _"Admission qourum factor", name = "quorum_factor", value = hint and 2 or nil }
slot.put("
")
ui.field.boolean{ label = _"Active?", name = "active", value = hint and true or nil }
ui.submit{ text = _"update area" }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/invite_list.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/admin/invite_list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,18 @@
+local members = Member:new_selector():exec()
+
+ui.list{
+ records = members,
+ columns = {
+ {
+ name = "id",
+ },
+ {
+ name = "invite_code",
+ },
+ {
+ content = function(member)
+ ui.link{ content = _"Invite letter", module = "admin", view = "invite_pdf", id = member.id }
+ end
+ }
+ }
+}
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/invite_pdf.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/admin/invite_pdf.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,25 @@
+local id = param.get_id()
+
+local member = Member:by_id(id)
+
+
+local luatex = require("luatex")
+luatex.temp_dir = WEBMCP_BASE_PATH .. "tmp"
+
+local tex = luatex.new_document()
+
+local template = config.invitation.template
+
+if type(template) == "function" then
+ template = template(member)
+else
+ template = template:gsub("#{invite_code}", member.invite_code)
+ template = template:gsub("#{url}", request.get_absolute_baseurl())
+end
+
+tex(template)
+
+local pdf = tex:get_pdf()
+
+slot.set_layout(nil, "application/pdf")
+slot.put_into("data", pdf)
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/member_edit.lua
--- a/app/main/admin/member_edit.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/member_edit.lua Sun Jul 15 14:07:29 2018 +0200
@@ -41,7 +41,8 @@
ui.sectionRow( function()
ui.field.text{ label = _"Identification", name = "identification" }
- ui.field.text{ label = _"Notification email", name = "notify_email" }
+ ui.field.text{ label = _"Notification email (confirmed)", name = "notify_email" }
+ ui.field.text{ label = _"Notification email (unconfirmed)", name = "notify_email_unconfirmed" }
if member and member.activated then
ui.field.text{ label = _"Screen name", name = "name" }
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/policy_show.lua
--- a/app/main/admin/policy_show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/policy_show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -38,9 +38,11 @@
ui.field.text{ label = _"Verification time", name = "verification_time", value = hint and "15 days" or nil }
ui.field.text{ label = _"Voting time", name = "voting_time", value = hint and "15 days" or nil }
- ui.field.text{ label = _"Issue quorum numerator", name = "issue_quorum_num", value = hint and "10" or nil }
- ui.field.text{ label = _"Issue quorum denominator", name = "issue_quorum_den", value = hint and "100" or nil }
+ ui.field.text{ label = _"Issue quorum", name = "issue_quorum", value = hint and "1" or nil }
+ ui.field.text{ label = _"Issue quorum numerator", name = "issue_quorum_num", value = hint and "1" or nil }
+ ui.field.text{ label = _"Issue quorum denominator", name = "issue_quorum_den", value = hint and "10" or nil }
+ ui.field.text{ label = _"Initiative quorum absolute", name = "initiative_quorum", value = hint and "1" or nil }
ui.field.text{ label = _"Initiative quorum numerator", name = "initiative_quorum_num", value = hint and "10" or nil }
ui.field.text{ label = _"Initiative quorum denominator", name = "initiative_quorum_den", value = hint and "100" or nil }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/unit_edit.lua
--- a/app/main/admin/unit_edit.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/admin/unit_edit.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,7 @@
local id = param.get_id()
+local hint = not id
+
local unit = Unit:by_id(id)
ui.titleAdmin(_"Organizational unit")
@@ -40,7 +42,7 @@
ui.field.text{ label = _"Name", name = "name" }
ui.field.text{ label = _"Description", name = "description", multiline = true }
ui.field.text{ label = _"External reference", name = "external_reference" }
- ui.field.boolean{ label = _"Active?", name = "active" }
+ ui.field.boolean{ label = _"Active?", name = "active", value = hint and true or nil }
slot.put(" ")
ui.submit{ text = _"update unit" }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/admin/verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,134 @@
+local verification = Verification:by_id(param.get_id())
+
+local data = {}
+
+for i, field in ipairs(config.verification.fields) do
+ table.insert(data, {
+ label = field.label,
+ value = verification.request_data[field.name]
+ })
+end
+
+table.insert(data, {
+ label = _"IP address",
+ value = verification.request_origin.ip
+})
+
+table.insert(data, {
+ label = _"Hostname",
+ value = verification.request_origin.hostname
+})
+
+if verification.verified then
+ table.insert(data, {
+ label = _"Requested at",
+ value = format.timestamp(verification.requested)
+ })
+end
+
+if verification.requesting_member_id then
+ table.insert(data, {
+ label = _"Requested by account",
+ value = verification.requesting_member_id
+ })
+end
+
+if verification.verified then
+ table.insert(data, {
+ label = _"Verified at",
+ value = format.timestamp(verification.verified)
+ })
+end
+
+if verification.denied then
+ table.insert(data, {
+ label = _"Denied at",
+ value = format.timestamp(verification.denied)
+ })
+end
+
+if verification.verifying_member_id then
+ table.insert(data, {
+ label = _"Verified by account",
+ value = verification.verifying_member_id
+ })
+end
+
+if verification.comment then
+ table.insert(data, {
+ label = _"Comment",
+ value = verification.comment
+ })
+end
+
+if verification.verified_member_id then
+ table.insert(data, {
+ label = _"Used by account",
+ value = verification.veried_member_id
+ })
+end
+
+ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ ui.list{
+ attr = { class = "mdl-data-table mdl-js-data-table mdl-shadow--2dp" },
+ records = data,
+ columns = {
+ {
+ label_attr = { class = "mdl-data-table__cell--non-numeric" },
+ field_attr = { class = "mdl-data-table__cell--non-numeric" },
+ label = _"Field",
+ content = function(record)
+ ui.tag{ content = record.label }
+ end
+ },
+ {
+ label_attr = { class = "mdl-data-table__cell--non-numeric" },
+ field_attr = { class = "mdl-data-table__cell--non-numeric" },
+ label = _"Value",
+ content = function(record)
+ ui.tag{ content = record.value }
+ end
+ },
+ }
+ }
+end }
+
+if not verification.verification_data and not verification.denied then
+ ui.form{
+ module = "admin", action = "verification_update", id = verification.id,
+ record = verification,
+ content = function()
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-verification_data", class = "mdl-textfield__input" },
+ label = _"Verification data",
+ name = "verification_data"
+ }
+ slot.put("
")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Verify account"
+ }
+ }
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "deny",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--accent",
+ value = _"Deny request"
+ }
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised" },
+ content = _"Cancel",
+ module = "admin", view = "verification_list"
+ }
+ end
+ }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/admin/verification_list.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/admin/verification_list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,70 @@
+ui.heading{ level = 1, content = _"Verification requests" }
+
+if not config.verification or not config.verification.fields then
+ return
+end
+
+
+local columns = {}
+
+for i, field in ipairs(config.verification.fields) do
+ table.insert(columns, {
+ label_attr = { class = "mdl-data-table__cell--non-numeric" },
+ field_attr = { class = "mdl-data-table__cell--non-numeric" },
+ label = field.label,
+ content = function(record)
+ ui.tag{ content = record.request_data[field.name] }
+ end
+ })
+end
+
+table.insert(columns, {
+ label = _"verified",
+ name = "verified"
+})
+
+table.insert(columns, {
+ label = _"denied",
+ name = "denied"
+})
+
+table.insert(columns, {
+ content = function(record)
+ ui.link{ content = _"show", module = "admin", view = "verification", id = record.id }
+ end
+})
+
+local new_verifications = Verification:new_selector():add_where("verified ISNULL and denied ISNULL"):exec()
+local verified_verifications = Verification:new_selector():add_where("verified NOTNULL"):exec()
+local denied_verifications = Verification:new_selector():add_where("denied NOTNULL"):exec()
+
+ui.container{ attr = { class = "mdl-tabs mdl-js-tabs mdl-js-ripple-effect" }, content = function()
+ ui.container{ attr = { class = "mdl-tabs__tab-bar" }, content = function()
+ ui.link{ content = _"new requests", external = "#new_requests", attr = { class = "mdl-tabs__tab is-active" } }
+ ui.link{ content = _"verified", external = "#verified", attr = { class = "mdl-tabs__tab" } }
+ ui.link{ content = _"denied", external = "#denied", attr = { class = "mdl-tabs__tab" } }
+ end }
+ slot.put(" ")
+ ui.container{ attr = { class = "mdl-tabs__panel is-active", id = "new_requests" }, content = function()
+ ui.list{
+ records = new_verifications,
+ attr = { class = "mdl-data-table mdl-js-data-table mdl-shadow--2dp" },
+ columns = columns
+ }
+ end }
+ ui.container{ attr = { class = "mdl-tabs__panel", id = "verified" }, content = function()
+ ui.list{
+ records = verified_verifications,
+ attr = { class = "mdl-data-table mdl-js-data-table mdl-shadow--2dp" },
+ columns = columns
+ }
+ end }
+ ui.container{ attr = { class = "mdl-tabs__panel", id = "denied" }, content = function()
+ ui.list{
+ records = denied_verifications,
+ attr = { class = "mdl-data-table mdl-js-data-table mdl-shadow--2dp" },
+ columns = columns
+ }
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/agent/_action/accept.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/agent/_action/accept.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,26 @@
+local controlled_id = param.get("controlled_id")
+
+local agent = Agent:by_pk(controlled_id, app.session.member_id)
+
+if not agent then
+ print("A")
+ return false
+end
+
+if agent.accepted ~= nil then
+ print("B")
+ return false
+end
+
+if param.get("rejected") then
+ print("C")
+ agent.accepted = false
+elseif param.get("accepted") then
+ print("D")
+ agent.accepted = true
+else
+ print("E")
+ return false
+end
+print("F")
+agent:save()
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/agent/show.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/agent/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,100 @@
+local controlled_id = param.get("controlled_id")
+
+
+ui.titleMember(_"Account access")
+
+ui.grid{ content = function()
+
+ ui.cell_main{ content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Account access" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local agent = Agent:new_selector()
+ :add_where{ "controller_id = ?", app.session.member_id }
+ :add_where{ "controlled_id = ?", controlled_id }
+ :optional_object_mode()
+ :exec()
+
+ if agent then
+
+ if agent.accepted == nil then
+ ui.container{ content = _"You have been granted access privileges for the following account:" }
+ elseif agent.accepted == true then
+ ui.container{ content = _"You have accepted access privileges for the following account:" }
+ elseif agent.accepted == false then
+ ui.container{ content = _"You have rejected access privileges for the following account:" }
+ end
+
+ slot.put(" ")
+ ui.link{
+ content = agent.controllee.display_name,
+ module = "member", view = "show", id = agent.controlled_id
+ }
+ slot.put("
")
+
+ ui.form{
+ attr = { class = "wide" },
+ module = "agent",
+ action = "accept",
+ params = { controlled_id = controlled_id },
+ routing = {
+ ok = {
+ mode = "redirect",
+ module = "agent",
+ view = "show",
+ params = { controlled_id = controlled_id },
+ }
+ },
+ content = function()
+
+ if agent.accepted == nil then
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Accept access privilege",
+ name = "accepted"
+ },
+ content = ""
+ }
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised",
+ value = _"Reject access privilege",
+ name = "rejected"
+ },
+ content = ""
+ }
+ end
+ slot.put(" ")
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button" },
+ module = "index", view = "index",
+ content = _"Cancel"
+ }
+ end
+ }
+
+ end
+
+ end }
+ end }
+ end }
+
+ ui.cell_sidebar{ content = function()
+ execute.view {
+ module = "member", view = "_sidebar_whatcanido", params = {
+ member = app.session.member
+ }
+ }
+ end }
+
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/_filter/30_auth.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/_filter/30_auth.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,48 @@
+local public_access_scopes = {
+ anonymous = "read_contents",
+ authors_pseudonymous = "read_contents read_authors",
+ all_pseudonymous = "read_contents read_authors read_ratings",
+ everything = "read_contents read_authors read_ratings read_identities read_profiles"
+}
+
+local access_token, access_token_err = util.get_access_token()
+
+if access_token_err then
+ if access_token_err == "header_and_param" then
+ return util.api_error(400, "Unauthorized", "invalid_request", "Access token passed both via header and param")
+ end
+ return util.api_error(500, "Internal server error", "internal_error", "Internal server error")
+end
+
+local scope
+
+if access_token then
+ local token = Token:by_token_type_and_token("access", access_token)
+ if token then
+ app.access_token = token
+ scope = token.scope
+ else
+ return util.api_error(401, "Unauthorized", "invalid_token", "The access token is invalid or expired")
+ end
+end
+
+if not scope then
+ scope = public_access_scopes[config.public_access]
+end
+
+if not scope then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Public access is not allowed at this instance.")
+end
+
+app.scopes = {}
+
+for scope in string.gmatch(scope, "[^ ]+") do
+ local match = string.match(scope, "(.+)_detached")
+ app.scopes[match or scope] = true
+end
+
+if not next(app.scopes) then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "No valid scope found")
+end
+
+execute.inner()
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/_issue.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/_issue.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,22 @@
+local issues = param.get("issues", "table")
+
+
+local fields = { "id", "area_id", "policy_id", "admin_notice", "external_reference", "state", "phase_finished", "created", "accepted", "half_frozen", "fully_frozen", "closed", "cleaned", "min_admission_time", "max_admission_time", "discussion_time", "verification_time", "voting_time", "latest_snapshot_id", "admission_snapshot_id", "half_freeze_snapshot_id", "full_freeze_snapshot_id", "population", "voter_count", "status_quo_schulze_rank" }
+
+local r = json.array()
+
+for i, issue in ipairs(issues) do
+ local ir = json.object()
+ for j, field in ipairs(fields) do
+ local value = issue[field]
+ if value == nil then
+ value = json.null
+ else
+ value = tostring(value)
+ end
+ ir[field] = value
+ end
+ r[#r+1] = ir
+end
+
+return r
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/_member.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/_member.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,45 @@
+local members = param.get("members", "table")
+
+local include_profile = param.get("include_profile", atom.boolean)
+
+if include_profile and not app.scopes.read_profiles then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope read_profiles required")
+end
+
+local fields = {}
+
+if app.scopes.read_authors or app.scopes.read_identities then
+ fields = { "id", "created", "last_activity", "admin", "name", "location" }
+end
+
+if app.scopes.read_identities then
+ fields[#fields+1] = "identification"
+end
+
+local r = json.array()
+
+if app.scopes.read_identities then
+
+ if include_profile then
+ members:load("profile")
+ end
+
+ for i, member in ipairs(members) do
+ local m = json.object()
+ for j, field in ipairs(fields) do
+ local value = member[field]
+ if value == nil then
+ value = json.null
+ else
+ value = tostring(value)
+ end
+ m[field] = value
+ end
+ if include_profile then
+ m.profile = execute.chunk{ module = "api", chunk = "_profile", params = { profile = member.profile } }
+ end
+ r[#r+1] = m
+ end
+end
+
+return r
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/_profile.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/_profile.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,22 @@
+local profile = param.get("profile", "table")
+
+local r = json.object()
+
+for i, field in ipairs(config.member_profile_fields) do
+ if profile.profile[field.id] then
+ r[field.id] = profile.profile[field.id] or json.null
+ end
+end
+--[[
+if profile.statement then
+ if request.get_param{ name = "statement_format" } == "html" then
+ r.statement = profile:get_content("html")
+ r.statement_format = "html"
+ else
+ r.statement = profile.statement
+ r.statement_format = profile.formatting_engine
+ end
+end
+--]]
+
+return r
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/_settings.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/_settings.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,11 @@
+local settings = param.get("settings", "table")
+
+local r = json.object()
+
+for i, field in ipairs(config.member_settings_fields) do
+ if settings.settings[field.id] ~= nil then
+ r[field.id] = settings.settings[field.id]
+ end
+end
+
+return r
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/application.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/application.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,40 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.array()
+
+local system_applications = SystemApplication:get_all()
+
+r[#r+1] = json.object{
+ type = "system",
+ name = "LiquidFeedback",
+ base_url = request.get_absolute_baseurl(),
+ manifest_url = request.get_absolute_baseurl() .. "api/1/info",
+ cert_common_name = config.oauth2.cert_common_name
+}
+
+for i, system_application in ipairs(system_applications) do
+ r[#r+1] = json.object{
+ type = "system",
+ name = system_application.name,
+ base_url = system_application.base_url,
+ manifest_url = system_application.manifest_url,
+ cert_common_name = system_application.cert_common_name
+ }
+end
+
+if app.access_token then
+
+ local member_applications = MemberApplication:by_member_id_with_domain(app.access_token.member_id)
+
+ for i, member_application in ipairs(member_applications) do
+ r[#r+1] = json.object{
+ type = "dynamic",
+ name = "https://" .. member_application.domain .. "/",
+ base_url = "https://" .. member_application.domain .. "/",
+ manifest_url = "https://" .. member_application.domain .. "/" .. config.oauth2.manifest_magic
+ }
+ end
+
+end
+
+slot.put_into("data", json.export(json.object{ result = r }))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/event.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/event.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,132 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object{
+ result = json.array{}
+}
+
+local selector = Event:new_selector()
+
+local member_id = param.get("member_id", atom.integer)
+local other_member_id = param.get("member_id", atom.integer)
+local scope = param.get("scope")
+local issue_id = param.get("issue_id", atom.integer)
+local state = param.get("state")
+local initiative_id = param.get("initiative_id", atom.integer)
+
+local include_members = param.get("include_members", atom.boolean)
+local include_other_members = param.get("include_other_members", atom.boolean)
+local include_profile = param.get("include_profile", atom.boolean)
+local include_issues = param.get("include_issues", atom.boolean)
+local include_initiatives = param.get("include_initiatives", atom.boolean)
+local include_drafts = param.get("include_drafts", atom.boolean)
+local include_suggestions = param.get("include_suggestions", atom.boolean)
+
+if member_id then
+ selector:add_where{ "member_id = ?", member_id }
+end
+
+if other_member_id then
+ selector:add_where{ "other_member_id = ?", other_member_id }
+end
+
+if scope then
+ selector:add_where{ "scope = ?", scope }
+end
+
+if issue_id then
+ selector:add_where{ "issue_id = ?", issue_id }
+end
+
+if scope then
+ selector:add_where{ "scope = ?", scope }
+end
+
+if initiative_id then
+ selector:add_where{ "initiative_id = ?", initiative_id }
+end
+
+selector:add_order_by("id DESC")
+
+local events = selector:exec()
+
+local member_ids = {}
+local issue_ids = {}
+local initiative_ids = {}
+local draft_ids = {}
+local suggestion_ids = {}
+
+for i, event in ipairs(events) do
+ local e = json.object()
+ e.id = event.id
+ e.occurrence = format.timestamp(event.occurrence)
+ e.event = event.event
+ e.member_id = event.member_id
+ e.other_member_id = event.other_member_id
+ e.scope = event.scope
+ e.issue_id = event.issue_id
+ e.state = event.state
+ e.initiative_id = event.initiative_id
+ e.draft_id = event.draft_id
+ e.suggestion_id = event.suggestion_id
+ e.value = event.value
+ if include_members and e.member_id then
+ member_ids[e.member_id] = true
+ end
+ if include_other_members and e.other_member_id then
+ member_ids[e.member_id] = true
+ end
+ if include_issues and e.issue_id then
+ issue_ids[e.issue_id] = true
+ end
+ if include_initiatives and e.initiative_id then
+ initiative_ids[e.initiative_id] = true
+ end
+ if include_drafts and e.draft_id then
+ draft_ids[e.draft_id] = true
+ end
+ if include_suggestions and e.suggestion_id then
+ suggestion_ids[e.suggestion_id] = true
+ end
+ r.result[#r.result+1] = e
+end
+
+function util.keys_to_array(tbl)
+ local r = {}
+ for k, v in pairs(tbl) do
+ r[#r+1] = k
+ end
+ return r
+end
+
+function util.array_to_json_object(tbl, key)
+ local r = json.object()
+ for i, v in ipairs(tbl) do
+ r[v[key]] = v
+ end
+ return r
+end
+
+if next(member_ids) then
+ local members = Member:by_ids(util.keys_to_array(member_ids))
+ r.members = util.array_to_json_object(
+ execute.chunk{ module = "api", chunk = "_member", params = { members = members, include_profile = include_profile } },
+ "id"
+ )
+ if r.members == false then
+ return
+ end
+end
+
+if next(issue_ids) then
+ local issues = Issue:by_ids(util.keys_to_array(issue_ids))
+ r.issues = util.array_to_json_object(
+ execute.chunk{ module = "api", chunk = "_issue", params = { issues = issues } },
+ "id"
+ )
+ if r.issues == false then
+ return
+ end
+end
+
+
+slot.put_into("data", json.export(r))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/info.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/info.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,53 @@
+slot.set_layout(nil, "application/json")
+
+local scope_string
+
+local scopes_list = {}
+for scope in pairs(app.scopes) do
+ scopes_list[#scopes_list+1] = scope
+end
+local scopes_string = table.concat(scopes_list, " ")
+
+local result = {}
+
+local r = json.object{
+ service = "LiquidFeedback",
+ core_version = db:query("SELECT * from liquid_feedback_version;")[1].string,
+ api_version = config.app_version,
+ client_tls_dn = request.get_header("X-SSL-DN"),
+ scope = scopes_string
+}
+
+if app.scopes.identification or app.scopes.authentication then
+ r.member_id = app.access_token.member_id
+ if app.access_token.member.role then
+ r.member_is_role = true
+ end
+ if app.access_token.session then
+ r.real_member_id = app.access_token.session.real_member_id
+ end
+ if param.get("include_member", atom.boolean) then
+ local member = app.access_token.member
+ result.member = json.object{
+ id = member.id,
+ name = member.name
+ }
+ if app.access_token.session and app.access_token.session.real_member then
+ result.real_member = json.object{
+ id = app.access_token.session.real_member.id,
+ name = app.access_token.session.real_member.name,
+ }
+ end
+ if app.scopes.identification then
+ result.member.identification = member.identification
+ if app.access_token.session and app.access_token.session.real_member then
+ result.real_member.identification = app.access_token.session.real_member.identification
+ end
+ end
+ end
+end
+
+result.result = r
+
+slot.put_into("data", json.export(result))
+slot.put_into("data", "\n")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/instance.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/instance.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,33 @@
+local navigation
+
+if param.get("include_navigation") then
+
+ local items = config.meta_navigation_items_func(
+ app.access_token and app.access_token.member or nil,
+ param.get("client_id"),
+ param.get("login_url")
+ )
+
+ navigation = json.array()
+ for i, item in ipairs(items) do
+ navigation[#navigation+1] = json.object{
+ name = item.name,
+ description = item.description,
+ url = item.url,
+ active = item.active
+ }
+ end
+
+end
+
+local result = json.object{
+ name = config.instance_name,
+ slogan = config.meta_navigation_slogan,
+ home_url = config.meta_navigation_home_url,
+ logo_url = config.meta_navigation_logo_url,
+ logo_alt_text = config.meta_navigation_logo_alt_text,
+ navigation = navigation
+}
+
+slot.set_layout(nil, "application/json")
+slot.put_into("data", json.export(json.object{ result = result }))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/member.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/member.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,20 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object{
+ result = json.array()
+}
+
+local selector = Member:new_selector()
+ :add_where("activated NOTNULL")
+ :add_order_by("id")
+
+if param.get("id") then
+ selector:add_where{ "id = ?", param.get("id") }
+end
+
+local members = selector:exec()
+local r.result = execute.chunk{ module = "api", chunk = "_member", params = { members = members } }
+
+
+slot.put_into("data", json.export(r))
+slot.put_into("data", "\n")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/navigation.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/navigation.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,34 @@
+local access_token = param.get("access_token")
+local client_id = param.get("client_id")
+local login_url = param.get("login_url")
+local format = param.get("format")
+
+if format ~= "html" and format ~= "raw_html" then
+ format = "json"
+end
+
+local items = config.meta_navigation_items_func(app.access_token and app.access_token.member or nil, client_id, login_url)
+
+if format == "json" then
+ slot.set_layout(nil, "application/json")
+ local r = json.array()
+ for i, item in ipairs(items) do
+ r[#r+1] = json.object{
+ name = item.name,
+ description = item.description,
+ url = item.url,
+ active = item.active
+ }
+ end
+ slot.put_into("data", json.export(json.object{ result = r }))
+elseif format == "html" then
+ slot.set_layout(nil, "application/json")
+ local html = config.meta_navigation_style_func(items) .. config.meta_navigation_html_func(items) .. config.meta_navigation_script_func(items)
+ slot.put_into("data", json.export(json.object{ result = html }))
+elseif format == "raw_html" then
+ slot.set_layout(nil, "text/html")
+ local html = config.meta_navigation_style_func(items) .. config.meta_navigation_html_func(items) .. config.meta_navigation_script_func(items)
+ slot.put_into("data", html)
+end
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/notify_email.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/notify_email.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,16 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object{}
+
+if not app.scopes.notify_email then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope notify_email required")
+end
+
+if app.access_token.member.notify_email ~= "" then
+ r.notify_email = app.access_token.member.notify_email
+else
+ r.notify_email = json.null
+end
+
+slot.put_into("data", json.export(json.object{ result = r }))
+slot.put_into("data", "\n")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/profile.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/profile.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,53 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object{}
+
+if request.is_post() then
+ if not app.scopes.update_profile then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope update_profile required")
+ end
+ local profile = app.access_token.member.profile
+ local fields = json.import(param.get("update"))
+ if not fields then
+ return util.api_error(400, "Bad Request", "profile_data_expected", "JSON object with updated profile data expected")
+ end
+ for i, field in ipairs(config.member_profile_fields) do
+ if json.type(fields, field.id) ~= "nil" then
+ local value = fields[field.id]
+ if value ~= nil and (field.type == "string" or field.type == "text") and json.type(value) ~= "string" then
+ return util.api_error(400, "Bad Request", "string_expected", "JSON encoded string value expected")
+ end
+ profile.profile[field.id] = value
+ end
+ end
+ profile:save()
+ r.status = 'ok'
+ slot.put_into("data", json.export(r))
+ slot.put_into("data", "\n")
+else
+ local member_id = tonumber(param.get("member_id"))
+ local profile
+ if member_id then
+ if not app.scopes.read_profiles then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope profile required")
+ end
+ local member = Member:by_id(member_id)
+ if not member then
+ return util.api_error(400, "Bad Request", "member_not_found", "No member with requested member_id")
+ end
+ profile = member.profile
+ elseif app.access_token then
+ if not app.scopes.profile and not app.scopes.read_profiles then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope profile required")
+ end
+ profile = app.access_token.member.profile
+ else
+ return util.api_error(400, "Bad Request", "no_member_id", "No member_id requested")
+ end
+ if profile then
+ r = execute.chunk{ module = "api", chunk = "_profile", params = { profile = profile } }
+ end
+ slot.put_into("data", json.export(json.object{ result = r }))
+ slot.put_into("data", "\n")
+end
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/profile_info.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/profile_info.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,16 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object()
+
+r.result = json.array()
+for i, field in ipairs(config.member_profile_fields) do
+ table.insert(r.result, json.object{
+ id = field.id,
+ name = field.name,
+ type = field.type
+ })
+end
+
+slot.put_into("data", json.export(r))
+slot.put_into("data", "\n")
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/settings.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/settings.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,50 @@
+slot.set_layout(nil, "application/json")
+
+if not app.access_token then
+ return util.api_error(400, "Forbidden", "insufficient_scope", "Scope 'settings' required")
+end
+
+local r = json.object{}
+
+if request.is_post() then
+ if not app.scopes.update_settings then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope update_settings required")
+ end
+ local settings = app.access_token.member.settings
+ if not settings then
+ settings = MemberSettings:new()
+ settings.member_id = app.access_token.member_id
+ settings.settings = json.object()
+ end
+ local fields = json.import(param.get("update"))
+ if not fields then
+ return util.api_error(400, "Bad Request", "settings_data_expected", "JSON object with updated settings data expected")
+ end
+ for i, field in ipairs(config.member_settings_fields) do
+ if json.type(fields, field.id) ~= "nil" then
+ local value = fields[field.id]
+ if value ~= nil then
+ if (field.type == "string" or field.type == "text") and json.type(value) ~= "string" then
+ return util.api_error(400, "Bad Request", "string_expected", "JSON encoded string value expected")
+ end
+ if (field.type == "boolean") and json.type(value) ~= "boolean" then
+ return util.api_error(400, "Bad Request", "boolean_expected", "JSON encoded boolean value expected")
+ end
+ end
+ settings.settings[field.id] = value
+ end
+ end
+ settings:save()
+ r.status = 'ok'
+ slot.put_into("data", json.export(r))
+ slot.put_into("data", "\n")
+else
+ if not app.scopes.settings then
+ return util.api_error(403, "Forbidden", "insufficient_scope", "Scope 'settings' required")
+ end
+ local settings = app.access_token.member.settings or json.object()
+ r = execute.chunk{ module = "api", chunk = "_settings", params = { settings = settings } }
+ slot.put_into("data", json.export(json.object{ result = r }))
+ slot.put_into("data", "\n")
+end
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/settings_info.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/settings_info.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,16 @@
+slot.set_layout(nil, "application/json")
+
+local r = json.object()
+
+r.result = json.array()
+for i, field in ipairs(config.member_settings_fields) do
+ table.insert(r.result, json.object{
+ id = field.id,
+ name = field.name,
+ type = field.type
+ })
+end
+
+slot.put_into("data", json.export(r))
+slot.put_into("data", "\n")
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/api/style.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/api/style.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,24 @@
+slot.set_layout(nil, "application/json")
+
+
+local r = json.object{
+ color = json.object()
+}
+
+local style = execute.chunk{ module = "style", chunk = "_style", params = { style = config.style } }
+
+if style.color_md then
+ r.color.md = {}
+ for k, v in pairs(style.color_md) do
+ r.color.md[k] = v
+ end
+end
+
+if style.color_rgb then
+ r.color.rgb = {}
+ for k, v in pairs(style.color_rgb) do
+ r.color.rgb[k] = v
+ end
+end
+
+slot.put_into("data", json.export(json.object{ result = r }))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/area/_head.lua
--- a/app/main/area/_head.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/area/_head.lua Sun Jul 15 14:07:29 2018 +0200
@@ -36,4 +36,4 @@
}
end }
-end )
\ No newline at end of file
+end )
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/area/_sidebar_members.lua
--- a/app/main/area/_sidebar_members.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/area/_sidebar_members.lua Sun Jul 15 14:07:29 2018 +0200
@@ -4,7 +4,6 @@
local area = param.get("area", "table")
local members_selector = Member:new_selector()
- :join("membership", nil, { "membership.member_id = member.id AND membership.area_id = ?", area.id })
:add_where("member.active")
:limit(50)
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/area/_sidebar_whatcanido.lua
--- a/app/main/area/_sidebar_whatcanido.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/area/_sidebar_whatcanido.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,10 +1,9 @@
-local member = param.get ( "member", "table" ) or app.session.member
-
local area = param.get ( "area", "table" )
+area:load_delegation_info_once_for_member_id(app.session.member_id)
local participating_trustee_id
local participating_trustee_name
-if member then
+if app.session.member then
if area.delegation_info.first_trustee_participation then
participating_trustee_id = area.delegation_info.first_trustee_id
participating_trustee_name = area.delegation_info.first_trustee_name
@@ -14,232 +13,192 @@
end
end
-ui.sidebar ( "tab-whatcanido", function ()
+ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"What can I do here?" }
+ end }
+ ui.container{ attr = { class = "what-can-i-do-here" }, content = function()
- ui.sidebarHeadWhatCanIDo()
-
- if member and not app.session.member:has_voting_right_for_unit_id(area.unit_id) then
- ui.sidebarSection( _"You are not entitled to vote in this unit" )
- end
-
- if member and app.session.member:has_voting_right_for_unit_id(area.unit_id) then
+ if app.session.member and app.session.member:has_voting_right_for_unit_id(area.unit_id) then
- if not app.session.member.disable_notifications then
-
- local ignored_area = IgnoredArea:by_pk(app.session.member_id, area.id)
+ if not app.session.member.disable_notifications then
+
+ local ignored_area = IgnoredArea:by_pk(app.session.member_id, area.id)
- if not ignored_area then
- ui.sidebarSection ( function ()
-
- ui.heading {
- level = 3,
- content = _"You are receiving updates by email for this subject area"
- }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.tag { content = function ()
- ui.link {
- module = "area", action = "update_ignore",
- params = { area_id = area.id },
- routing = { default = {
- mode = "redirect", module = "area", view = "show", id = area.id
- } },
- text = _"unsubscribe from update emails about this area"
- }
+ if not ignored_area then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"You are receiving updates by email for this subject area" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.tag { content = function ()
+ ui.link {
+ module = "area", action = "update_ignore",
+ params = { area_id = area.id },
+ routing = { default = {
+ mode = "redirect", module = "index", view = "index", params = { unit = area.unit_id, area = area.id }
+ } },
+ text = _"unsubscribe from update emails about this area"
+ }
+ end }
end }
end }
end }
- end )
- end
+ end
+
+ if ignored_area then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to stay informed" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.tag { content = function ()
+ ui.link {
+ module = "area", action = "update_ignore",
+ params = { area_id = area.id, delete = true },
+ routing = { default = {
+ mode = "redirect", module = "index", view = "index", params = { unit = area.unit_id, area = area.id }
+ } },
+ text = _"subscribe for update emails about this area"
+ }
+ end }
+ end }
+ end }
+ end }
+ end
- if ignored_area then
- ui.sidebarSection ( function ()
-
- ui.heading {
- level = 3,
- content = _"I want to stay informed"
- }
+ else
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to stay informed about this subject area" }
ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
ui.tag { tag = "li", content = function ()
ui.tag { content = function ()
+ ui.tag{ content = _"Edit your global " }
ui.link {
- module = "area", action = "update_ignore",
- params = { area_id = area.id, delete = true },
- routing = { default = {
- mode = "redirect", module = "area", view = "show", id = area.id
- } },
- text = _"subscribe for update emails about this area"
+ module = "member", view = "settings_notification",
+ params = { return_to = "area", return_to_area_id = area.id },
+ text = _"notification settings"
}
+ ui.tag{ content = _" to receive updates by email" }
end }
end }
end }
- end )
- end
-
- else
- ui.sidebarSection ( function ()
-
- ui.heading {
- level = 3,
- content = _"I want to stay informed about this subject area"
- }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.tag { content = function ()
- ui.tag{ content = _"Edit your global " }
- ui.link {
- module = "member", view = "settings_notification",
- params = { return_to = "area", return_to_area_id = area.id },
- text = _"notification settings"
- }
- ui.tag{ content = _" to receive updates by email" }
- end }
- end }
- end }
- end )
- end
-
- if area.delegation_info.own_participation then
- ui.sidebarSection ( function ()
- ui.image{ attr = { class = "right" }, static = "icons/48/star.png" }
- ui.heading {
- level = 3,
- content = _"You are subscribed for this subject area"
- }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.tag { content = function ()
- ui.link {
- module = "membership", action = "update",
- routing = { default = {
- mode = "redirect", module = "area", view = "show", id = area.id
- } },
- params = { area_id = area.id, delete = true },
- text = _"unsubscribe"
- }
- end }
- end }
end }
- end )
- end
-
- if not area.delegation_info.own_participation then
- ui.sidebarSection ( function ()
+ end
- ui.heading {
- level = 3,
- content = _"I want to participate in this subject area"
- }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.tag { content = function ()
- ui.link {
- module = "membership", action = "update",
- routing = { default = {
- mode = "redirect", module = "area", view = "show", id = area.id
- } },
- params = { area_id = area.id },
- text = _"subscribe"
- }
- end }
+ if app.session.member:has_voting_right_for_unit_id ( area.unit_id ) then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to vote" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"check the issues on the right, and click on 'Vote now' to vote on an issue which is in voting phase." }
end }
end }
- end )
- end
+ end
+
+ if app.session.member and not app.session.member:has_voting_right_for_unit_id(area.unit_id) then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = _"You are not entitled to vote in this unit" }
+ end
-
-
- ui.sidebarSection ( function ()
-
+ if app.session.member and app.session.member:has_voting_right_for_unit_id(area.unit_id) then
+
+ if not config.disable_delegations then
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ if not area.delegation_info.first_trustee_id then
+ ui.tag{ content = _"I want to delegate this subject area" }
+ else
+ ui.container { attr = { class = "right" }, content = function()
+ local member = Member:by_id(area.delegation_info.first_trustee_id)
+ execute.view{
+ module = "member_image",
+ view = "_show",
+ params = {
+ member = member,
+ image_type = "avatar",
+ show_dummy = true
+ }
+ }
+ end }
+ ui.tag{ content = _"You delegated this subject area" }
+ end
- if not area.delegation_info.first_trustee_id then
- ui.heading{ level = 3, content = _"I want to delegate this subject area" }
- else
- ui.container { attr = { class = "right" }, content = function()
- local member = Member:by_id(area.delegation_info.first_trustee_id)
- execute.view{
- module = "member_image",
- view = "_show",
- params = {
- member = member,
- image_type = "avatar",
- show_dummy = true
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ if area.delegation_info.own_delegation_scope == "unit" then
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ unit_id = area.unit_id,
+ },
+ content = _("change/revoke delegation of organizational unit")
+ }
+ end }
+ end
+
+ if area.delegation_info.own_delegation_scope == nil then
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = _"choose subject area delegatee"
+ }
+ end }
+ elseif area.delegation_info.own_delegation_scope == "area" then
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = _"change/revoke area delegation"
+ }
+ end }
+ else
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = _"change/revoke delegation only for this subject area"
+ }
+ end }
+ end
+ end }
+ end }
+ end
+
+ if app.session.member:has_initiative_right_for_unit_id ( area.unit_id ) then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{
+ content = _("I want to start a new initiative", {
+ area_name = area.name
+ } )
}
- }
- end }
- ui.heading{ level = 3, content = _"You delegated this subject area" }
- end
-
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- if area.delegation_info.own_delegation_scope == "unit" then
- ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- unit_id = area.unit_id,
- },
- content = _("change/revoke delegation of organizational unit")
- }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"Take a look through the existing issues. Maybe someone else started a debate on your topic (and you can join it) or the topic has been decided already in the past." }
+ ui.tag { tag = "li", content = function ()
+ ui.tag { content = function ()
+ ui.tag { content = _"If you cannot find any appropriate existing issue, " }
+ ui.link {
+ module = "initiative", view = "new",
+ params = { area_id = area.id },
+ text = _"start an initiative in a new issue"
+ }
+ end }
+ end }
+ end }
end }
end
-
- if area.delegation_info.own_delegation_scope == nil then
- ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- area_id = area.id
- },
- content = _"choose subject area delegatee"
- }
- end }
- elseif area.delegation_info.own_delegation_scope == "area" then
- ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- area_id = area.id
- },
- content = _"change/revoke area delegation"
- }
- end }
- else
- ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- area_id = area.id
- },
- content = _"change/revoke delegation only for this subject area"
- }
- end }
- end
- end }
- end )
-
-
-
-
- if app.session.member:has_voting_right_for_unit_id ( area.unit_id ) then
- ui.sidebarSection ( function ()
- ui.heading {
- level = 3,
- content = _("I want to start a new initiative", {
- area_name = area.name
- } )
- }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = _"Take a look through the existing issues. Maybe someone else started a debate on your topic (and you can join it) or the topic has been decided already in the past." }
- ui.tag { tag = "li", content = function ()
- ui.tag { content = function ()
- ui.tag { content = _"If you cannot find any appropriate existing issue, " }
- ui.link {
- module = "initiative", view = "new",
- params = { area_id = area.id },
- text = _"start an initiative in a new issue"
- }
- end }
+
+ end
+ else
+ ui.container { attr = { class = "mdl-card__content mdl-card--border" }, content = function ()
+ ui.tag{ content = _"You are not entitled to vote in this unit" }
+ ui.tag{ tag = "ul", content = function()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "index", view = "login", content = _"Login" }
end }
end }
- end )
+ end }
end
- else
- end
+ end }
-end )
\ No newline at end of file
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/area/show.lua
--- a/app/main/area/show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/area/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -6,59 +6,68 @@
return
end
+app.current_area = area
+
+
area:load_delegation_info_once_for_member_id(app.session.member_id)
app.html_title.title = area.name
app.html_title.subtitle = _("Area")
-execute.view {
- module = "area", view = "_head", params = {
- area = area, member = app.session.member
- }
-}
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ ui.heading{ content = area.unit.name .. " / " .. area.name }
-execute.view {
- module = "area", view = "_sidebar_whatcanido", params = {
- area = area
- }
-}
+ execute.view {
+ module = "area", view = "_head", params = {
+ area = area, member = app.session.member
+ }
+ }
+
+ execute.view {
+ module = "area", view = "_sidebar_whatcanido", params = {
+ area = area
+ }
+ }
-execute.view {
- module = "area", view = "_sidebar_members", params = {
- area = area
- }
-}
+ execute.view {
+ module = "area", view = "_sidebar_members", params = {
+ area = area
+ }
+ }
-local function getOpenIssuesSelector()
- return area:get_reference_selector("issues")
- :add_order_by("coalesce(issue.fully_frozen + issue.voting_time, issue.half_frozen + issue.verification_time, issue.accepted + issue.discussion_time, issue.created + issue.max_admission_time) - now()")
-end
+ local function getOpenIssuesSelector()
+ return area:get_reference_selector("issues")
+ :add_order_by("coalesce(issue.fully_frozen + issue.voting_time, issue.half_frozen + issue.verification_time, issue.accepted + issue.discussion_time, issue.created + issue.max_admission_time) - now()")
+ end
-local admission_selector = getOpenIssuesSelector()
- :add_where("issue.state = 'admission'");
+ local admission_selector = getOpenIssuesSelector()
+ :add_where("issue.state = 'admission'");
-local discussion_selector = getOpenIssuesSelector()
- :add_where("issue.state = 'discussion'");
+ local discussion_selector = getOpenIssuesSelector()
+ :add_where("issue.state = 'discussion'");
-local verification_selector = getOpenIssuesSelector()
- :add_where("issue.state = 'verification'");
+ local verification_selector = getOpenIssuesSelector()
+ :add_where("issue.state = 'verification'");
-local voting_selector = getOpenIssuesSelector()
- :add_where("issue.state = 'voting'");
+ local voting_selector = getOpenIssuesSelector()
+ :add_where("issue.state = 'voting'");
-local closed_selector = area:get_reference_selector("issues")
- :add_where("issue.closed NOTNULL")
- :add_order_by("issue.closed DESC")
+ local closed_selector = area:get_reference_selector("issues")
+ :add_where("issue.closed NOTNULL")
+ :add_order_by("issue.closed DESC")
-local members_selector = area:get_reference_selector("members"):add_where("member.active")
-local delegations_selector = area:get_reference_selector("delegations")
- :join("member", "truster", "truster.id = delegation.truster_id AND truster.active")
- :join("member", "trustee", "trustee.id = delegation.trustee_id AND trustee.active")
+ local members_selector = area:get_reference_selector("members"):add_where("member.active")
+ local delegations_selector = area:get_reference_selector("delegations")
+ :join("member", "truster", "truster.id = delegation.truster_id AND truster.active")
+ :join("member", "trustee", "trustee.id = delegation.trustee_id AND trustee.active")
-execute.view {
- module = "issue",
- view = "_list2",
- params = { for_area = area }
-}
+ execute.view {
+ module = "issue",
+ view = "_list",
+ params = { for_area = area }
+ }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/contact/list.lua
--- a/app/main/contact/list.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/contact/list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -5,97 +5,112 @@
ui.title(_"Contacts")
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
-ui.paginate{
- selector = contacts_selector,
- content = function()
- local contacts = contacts_selector:exec()
- if #contacts == 0 then
- ui.field.text{ value = _"You didn't save any member as contact yet." }
- else
- ui.list{
- records = contacts,
- columns = {
- {
- label = _"Name",
- content = function(record)
- ui.link{
- text = record.other_member.name,
- module = "member",
- view = "show",
- id = record.other_member_id
- }
- end
- },
- {
- label = _"Published",
- content = function(record)
- ui.field.boolean{ value = record.public }
- end
- },
- {
- content = function(record)
- if record.public then
- ui.link{
- attr = { class = "action" },
- text = _"Hide",
- module = "contact",
- action = "add_member",
- id = record.other_member_id,
- params = { public = false },
- routing = {
- default = {
- mode = "redirect",
- module = request.get_module(),
- view = request.get_view(),
- id = request.get_id_string(),
- params = request.get_param_strings()
- }
- }
- }
- else
- ui.link{
- attr = { class = "action" },
- text = _"Publish",
- module = "contact",
- action = "add_member",
- id = record.other_member_id,
- params = { public = true },
- routing = {
- default = {
- mode = "redirect",
- module = request.get_module(),
- view = request.get_view(),
- id = request.get_id_string(),
- params = request.get_param_strings()
- }
- }
- }
- end
- end
- },
- {
- content = function(record)
- ui.link{
- attr = { class = "action" },
- text = _"Remove",
- module = "contact",
- action = "remove_member",
- id = record.other_member_id,
- routing = {
- default = {
- mode = "redirect",
- module = request.get_module(),
- view = request.get_view(),
- id = request.get_id_string(),
- params = request.get_param_strings()
- }
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Contacts" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+
+ ui.paginate{
+ selector = contacts_selector,
+ content = function()
+ local contacts = contacts_selector:exec()
+ if #contacts == 0 then
+ ui.field.text{ value = _"You didn't save any member as contact yet." }
+ else
+ ui.list{
+ attr = { class = "mdl-data-table mdl-js-data-table mdl-shadow--2dp" },
+ records = contacts,
+ columns = {
+ {
+ label = _"Name",
+ content = function(record)
+ ui.link{
+ text = record.other_member.name,
+ module = "member",
+ view = "show",
+ id = record.other_member_id
+ }
+ end
+ },
+ {
+ label = _"Published",
+ content = function(record)
+ ui.field.boolean{ value = record.public }
+ end
+ },
+ {
+ content = function(record)
+ if record.public then
+ ui.link{
+ attr = { class = "action" },
+ text = _"Hide",
+ module = "contact",
+ action = "add_member",
+ id = record.other_member_id,
+ params = { public = false },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = request.get_module(),
+ view = request.get_view(),
+ id = request.get_id_string(),
+ params = request.get_param_strings()
+ }
+ }
+ }
+ else
+ ui.link{
+ attr = { class = "action" },
+ text = _"Publish",
+ module = "contact",
+ action = "add_member",
+ id = record.other_member_id,
+ params = { public = true },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = request.get_module(),
+ view = request.get_view(),
+ id = request.get_id_string(),
+ params = request.get_param_strings()
+ }
+ }
+ }
+ end
+ end
+ },
+ {
+ content = function(record)
+ ui.link{
+ attr = { class = "action" },
+ text = _"Remove",
+ module = "contact",
+ action = "remove_member",
+ id = record.other_member_id,
+ routing = {
+ default = {
+ mode = "redirect",
+ module = request.get_module(),
+ view = request.get_view(),
+ id = request.get_id_string(),
+ params = request.get_param_strings()
+ }
+ }
+ }
+ end
+ },
}
}
end
- },
+ end
}
- }
- end
- end
-}
+
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/delegation/_action/update.lua
--- a/app/main/delegation/_action/update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/delegation/_action/update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,3 +1,7 @@
+if config.disable_delegations then
+ return
+end
+
local truster_id = app.session.member.id
local trustee_id = param.get("trustee_id", atom.integer)
@@ -58,7 +62,7 @@
end
if not app.session.member:has_voting_right_for_unit_id(check_unit_id) then
- error("access denied")
+ return execute.view { module = "index", view = "403" }
end
if not delegation then
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/delegation/_info.lua
--- a/app/main/delegation/_info.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/delegation/_info.lua Sun Jul 15 14:07:29 2018 +0200
@@ -174,7 +174,7 @@
if info.own_participation then
if issue and issue.fully_frozen then
ui.link{
- attr = { class = "right" },
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
module = "vote", view = "list", params = {
issue_id = issue.id
},
@@ -184,8 +184,9 @@
}
else
if issue then
- local text = _"you are interested"
- ui.image { attr = { class = "star", title = text, alt = text }, static = "icons/48/eye.png" }
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "star" }
+ slot.put(" ")
+ ui.tag{ content = _"you are interested" }
if not issue.closed and info.own_participation and info.weight and info.weight > 1 then
ui.link {
attr = { class = "right" }, content = "+" .. (info.weight - 1),
@@ -198,11 +199,7 @@
local text = _"you are subscribed"
ui.image { attr = { class = "icon24 star", title = text, alt = text }, static = "icons/48/star.png" }
end
- if not for_title then
- slot.put(" ")
- else
- slot.put(" ")
- end
+ slot.put(" ")
end
end
end
@@ -260,3 +257,15 @@
end
end
+if issue and app.session.member and issue.fully_frozen and not issue.closed and not issue.member_info.direct_voted and app.session.member:has_voting_right_for_unit_id(issue.area.unit_id) then
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "vote", view = "list", params = {
+ issue_id = issue.id
+ },
+ content = function ()
+ ui.tag { content = _"vote now" }
+ end
+ }
+ else
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/delegation/show.lua
--- a/app/main/delegation/show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/delegation/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -113,283 +113,303 @@
ui.script{ static = "js/update_delegation_info.js" }
-ui.section( function ()
-
- ui.sectionHead( function ()
- ui.heading{ level = 1, content = head_text }
- end )
-
- ui.sectionRow( function ()
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--8-col" }, content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = head_text }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+
+ ui.form{
+ attr = { class = "wide section", id = "delegationForm" },
+ module = "delegation",
+ action = "update",
+ params = {
+ unit_id = unit and unit.id or nil,
+ area_id = area and area.id or nil,
+ issue_id = issue and issue.id or nil,
+ initiative_id = initiative_id
+ },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = area and "area" or initiative and "initiative" or issue and "issue" or "unit",
+ view = "show",
+ id = area and area.id or initiative and initiative.id or issue and issue.id or unit.id,
+ }
+ },
+ content = function()
+ local records
+ if issue then
+ local delegate_name = ""
+ local scope = _"no delegation set"
+ local area_delegation = Delegation:by_pk(app.session.member_id, nil, issue.area_id)
+ if area_delegation then
+ delegate_name = area_delegation.trustee and area_delegation.trustee.name or _"abandoned"
+ scope = _"area"
+ else
+ local unit_delegation = Delegation:by_pk(app.session.member_id, issue.area.unit_id)
+ if unit_delegation then
+ delegate_name = unit_delegation.trustee.name
+ scope = config.single_unit_id and _"global" or _"unit"
+ end
+ end
+ local text_apply
+ local text_abandon
+ if config.single_unit_id then
+ text_apply = _("Apply global or area delegation for this issue (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
+ text_abandon = _"Abandon unit and area delegations for this issue"
+ else
+ text_apply = _("Apply unit or area delegation for this issue (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
+ text_abandon = _"Abandon unit and area delegations for this issue"
+ end
+
+ records = {
+ { id = -1, name = text_apply },
+ { id = 0, name = text_abandon }
+ }
+ elseif area then
+ local delegate_name = ""
+ local scope = _"no delegation set"
+ local unit_delegation = Delegation:by_pk(app.session.member_id, area.unit_id)
+ if unit_delegation then
+ delegate_name = unit_delegation.trustee.name
+ scope = config.single_unit_id and _"global" or _"unit"
+ end
+ local text_apply
+ local text_abandon
+ if config.single_unit_id then
+ text_apply = _("Apply global delegation for this area (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
+ text_abandon = _"Abandon global delegation for this area"
+ else
+ text_apply = _("Apply unit delegation for this area (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
+ text_abandon = _"Abandon unit delegation for this area"
+ end
+ records = {
+ {
+ id = -1,
+ name = text_apply
+ },
+ {
+ id = 0,
+ name = text_abandon
+ }
+ }
+
+ else
+ records = {
+ {
+ id = -1,
+ name = _"No delegation"
+ }
+ }
- ui.form{
- attr = { class = "wide section", id = "delegationForm" },
- module = "delegation",
- action = "update",
- params = {
- unit_id = unit and unit.id or nil,
- area_id = area and area.id or nil,
- issue_id = issue and issue.id or nil,
- initiative_id = initiative_id
- },
- routing = {
- default = {
- mode = "redirect",
- module = area and "area" or initiative and "initiative" or issue and "issue" or "unit",
- view = "show",
- id = area and area.id or initiative and initiative.id or issue and issue.id or unit.id,
- }
- },
- content = function()
- local records
- if issue then
- local delegate_name = ""
- local scope = _"no delegation set"
- local area_delegation = Delegation:by_pk(app.session.member_id, nil, issue.area_id)
- if area_delegation then
- delegate_name = area_delegation.trustee and area_delegation.trustee.name or _"abandoned"
- scope = _"area"
- else
- local unit_delegation = Delegation:by_pk(app.session.member_id, issue.area.unit_id)
- if unit_delegation then
- delegate_name = unit_delegation.trustee.name
- scope = config.single_unit_id and _"global" or _"unit"
+ end
+ -- add current trustee
+ if current_trustee_id then
+ records[#records+1] = {id="_", name= "--- " .. _"Current delegatee" .. " ---"}
+ records[#records+1] = { id = current_trustee_id, name = current_trustee_name }
+ end
+ -- add initiative authors
+ if initiative then
+ records[#records+1] = {id="_", name= "--- " .. _"Initiators" .. " ---"}
+ for i,record in ipairs(initiative.initiators) do
+ records[#records+1] = record.member
+ end
+ end
+ -- add saved members
+ if #contact_members > 0 then
+ records[#records+1] = {id="_", name= "--- " .. _"Saved contacts" .. " ---"}
+ for i, record in ipairs(contact_members) do
+ records[#records+1] = record
+ end
+ end
+
+ local disabled_records = {}
+ disabled_records["_"] = true
+ disabled_records[app.session.member_id] = true
+
+ local value = current_trustee_id
+ if preview_trustee_id then
+ value = preview_trustee_id
+ end
+ if preview_trustee_id == nil and delegation and not delegation.trustee_id then
+ value = 0
+ end
+
+ ui.tag{ content = _"Choose your delegatee" }
+
+ ui.field.select{
+ attr = { onchange = "updateDelegationInfo();" },
+ name = "trustee_id",
+ foreign_records = records,
+ foreign_id = "id",
+ foreign_name = "name",
+ disabled_records = disabled_records,
+ value = value
+ }
+ slot.put(" ")
+
+ ui.container{ content = _"You can choose only members which you have been saved as contact before." }
+
+ ui.field.hidden{ name = "preview" }
+
+ slot.put(" ")
+ ui.tag { tag = "input", content = "", attr = {
+ type = "submit",
+ value = _"Save",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ } }
+
+ slot.put(" ")
+ if initiative then
+ ui.link{
+ module = "initiative",
+ view = "show",
+ id = initiative.id,
+ attr = { class = "mdl-button mdl-js-button mdl-button--underlined" },
+ content = function()
+ slot.put(_"Cancel")
+ end,
+ }
+ elseif issue then
+ ui.link{
+ module = "issue",
+ view = "show",
+ id = issue.id,
+ attr = { class = "mdl-button mdl-js-button mdl-button--underlined" },
+ content = function()
+ slot.put(_"Cancel")
+ end,
+ }
+ elseif area then
+ ui.link{
+ module = "index",
+ view = "index",
+ params = { unit = area.unit_id, area = area.id },
+ attr = { class = "mdl-button mdl-js-button mdl-button--underlined" },
+ content = function()
+ slot.put(_"Cancel")
+ end,
+ }
+ else
+ ui.link{
+ module = "index",
+ view = "index",
+ attr = { class = "mdl-button mdl-js-button mdl-button--underlined" },
+ content = function()
+ slot.put(_"Cancel")
+ end,
+ }
+ end
+
end
- end
- local text_apply
- local text_abandon
- if config.single_unit_id then
- text_apply = _("Apply global or area delegation for this issue (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
- text_abandon = _"Abandon unit and area delegations for this issue"
- else
- text_apply = _("Apply unit or area delegation for this issue (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
- text_abandon = _"Abandon unit and area delegations for this issue"
- end
-
- records = {
- { id = -1, name = text_apply },
- { id = 0, name = text_abandon }
- }
- elseif area then
- local delegate_name = ""
- local scope = _"no delegation set"
- local unit_delegation = Delegation:by_pk(app.session.member_id, area.unit_id)
- if unit_delegation then
- delegate_name = unit_delegation.trustee.name
- scope = config.single_unit_id and _"global" or _"unit"
- end
- local text_apply
- local text_abandon
- if config.single_unit_id then
- text_apply = _("Apply global delegation for this area (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
- text_abandon = _"Abandon global delegation for this area"
- else
- text_apply = _("Apply unit delegation for this area (Currently: #{delegate_name} [#{scope}])", { delegate_name = delegate_name, scope = scope })
- text_abandon = _"Abandon unit delegation for this area"
- end
- records = {
- {
- id = -1,
- name = text_apply
- },
- {
- id = 0,
- name = text_abandon
- }
- }
-
- else
- records = {
- {
- id = -1,
- name = _"No delegation"
- }
}
- end
- -- add current trustee
- if current_trustee_id then
- records[#records+1] = {id="_", name= "--- " .. _"Current delegatee" .. " ---"}
- records[#records+1] = { id = current_trustee_id, name = current_trustee_name }
- end
- -- add initiative authors
- if initiative then
- records[#records+1] = {id="_", name= "--- " .. _"Initiators" .. " ---"}
- for i,record in ipairs(initiative.initiators) do
- records[#records+1] = record.member
- end
- end
- -- add saved members
- if #contact_members > 0 then
- records[#records+1] = {id="_", name= "--- " .. _"Saved contacts" .. " ---"}
- for i, record in ipairs(contact_members) do
- records[#records+1] = record
- end
- end
-
- local disabled_records = {}
- disabled_records["_"] = true
- disabled_records[app.session.member_id] = true
-
- local value = current_trustee_id
- if preview_trustee_id then
- value = preview_trustee_id
- end
- if preview_trustee_id == nil and delegation and not delegation.trustee_id then
- value = 0
- end
+ end }
+ end }
+ end }
+ -- ------------------------
- ui.heading{ level = 2, content = _"Choose your delegatee" }
-
- ui.field.select{
- attr = { onchange = "updateDelegationInfo();" },
- name = "trustee_id",
- foreign_records = records,
- foreign_id = "id",
- foreign_name = "name",
- disabled_records = disabled_records,
- value = value
- }
-
- ui.container{ content = _"You can choose only members which you have been saved as contact before." }
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col mdl-cell--4-col-desktop" }, content = function()
+ ui.container{ attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
- ui.field.hidden{ name = "preview" }
-
- slot.put(" ")
- ui.tag { tag = "input", content = "", attr = {
- type = "submit",
- value = _"Save",
- class = "btn btn-default",
- } }
-
- slot.put("
")
- if initiative then
- ui.link{
- module = "initiative",
- view = "show",
- id = initiative.id,
- content = function()
- slot.put(_"Cancel")
- end,
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.container{
+ attr = { class = "mdl-card__title-text" },
+ content = _"Preview of delegation"
}
- elseif issue then
- ui.link{
- module = "issue",
- view = "show",
- id = issue.id,
- content = function()
- slot.put(_"Cancel")
- end,
- }
- elseif area then
- ui.link{
- module = "area",
- view = "show",
- id = area.id,
- content = function()
- slot.put(_"Cancel")
- end,
- }
- else
- ui.link{
- module = "index",
- view = "index",
- content = function()
- slot.put(_"Cancel")
- end,
- }
- end
+ end }
- end
- }
-
-end ) end )
--- ------------------------
-
-ui.sidebar( "tab-members", function ()
-
-ui.sidebarHead( function ()
- ui.heading { level = 1, content = _"Preview of delegation" }
-end )
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ local preview_inherit = false
+ local tmp_preview_trustee_id = preview_trustee_id
+ if preview_trustee_id == -1 then
+ preview_inherit = true
+ tmp_preview_trustee_id = nil
+ end
+ local delegation_chain = Member:new_selector()
+ :add_field("delegation_chain.*")
+ :join({ "delegation_chain(?,?,?,?,?,?)", app.session.member.id, unit_id, area_id, issue_id, tmp_preview_trustee_id, preview_inherit }, "delegation_chain", "member.id = delegation_chain.member_id")
+ :add_order_by("index")
+ :exec()
-local preview_inherit = false
-local tmp_preview_trustee_id = preview_trustee_id
-if preview_trustee_id == -1 then
- preview_inherit = true
- tmp_preview_trustee_id = nil
-end
-local delegation_chain = Member:new_selector()
- :add_field("delegation_chain.*")
- :join({ "delegation_chain(?,?,?,?,?,?)", app.session.member.id, unit_id, area_id, issue_id, tmp_preview_trustee_id, preview_inherit }, "delegation_chain", "member.id = delegation_chain.member_id")
- :add_order_by("index")
- :exec()
+ for i, record in ipairs(delegation_chain) do
+ local style
+ local overridden = (not issue or issue.state ~= 'voting') and record.overridden
+ ui.sidebarSection( function ()
+ if record.scope_in then
+ if not overridden then
+ local text = _"delegated to"
+ ui.image{
+ attr = { class = "delegation_arrow", alt = text, title = text },
+ static = "delegation_arrow_24_vertical.png"
+ }
+ else
+ local text = _"delegated to"
+ ui.image{
+ attr = { class = "delegation_arrow delegation_arrow_overridden", alt = text, title = text },
+ static = "delegation_arrow_24_vertical.png"
+ }
+ end
+ ui.tag{
+ attr = { class = "delegation_scope" .. (overridden and " delegation_scope_overridden" or "") },
+ content = function()
+ if record.scope_in == "unit" then
+ slot.put(config.single_object_mode and _"Global delegation" or _"Unit delegation")
+ elseif record.scope_in == "area" then
+ slot.put(_"Area delegation")
+ elseif record.scope_in == "issue" then
+ slot.put(_"Issue delegation")
+ end
+ end
+ }
+ end
+ ui.container{
+ attr = { class = overridden and "delegation_overridden" or "" },
+ content = function()
+ execute.view{
+ module = "member",
+ view = "_show_thumb",
+ params = { member = record }
+ }
+ end
+ }
+ if issue and issue.state ~= 'voting' and record.participation and not record.overridden then
+ ui.container{
+ attr = { class = "delegation_participation" },
+ content = function()
+ if i == #delegation_chain then
+ ui.tag{ content = _"This member is currently participating in this issue." }
+ else
+ ui.tag{ content = _"This member is participating, the remaining delegation chain is suspended during discussing." }
+ end
+ end
+ }
+ end
+ slot.put(" ")
+ end )
+ end
-for i, record in ipairs(delegation_chain) do
- local style
- local overridden = (not issue or issue.state ~= 'voting') and record.overridden
- ui.sidebarSection( function ()
- if record.scope_in then
- if not overridden then
- local text = _"delegated to"
- ui.image{
- attr = { class = "delegation_arrow", alt = text, title = text },
- static = "delegation_arrow_24_vertical.png"
- }
- else
- local text = _"delegated to"
- ui.image{
- attr = { class = "delegation_arrow delegation_arrow_overridden", alt = text, title = text },
- static = "delegation_arrow_24_vertical.png"
- }
- end
- ui.tag{
- attr = { class = "delegation_scope" .. (overridden and " delegation_scope_overridden" or "") },
- content = function()
- if record.scope_in == "unit" then
- slot.put(config.single_object_mode and _"Global delegation" or _"Unit delegation")
- elseif record.scope_in == "area" then
- slot.put(_"Area delegation")
- elseif record.scope_in == "issue" then
- slot.put(_"Issue delegation")
+ if preview_trustee_id == 0 or not preview_trustee_id == null and delegation and not delegation.trustee_id then
+ ui.image{
+ static = "icons/16/table_go_crossed.png"
+ }
+ if issue_id then
+ slot.put(_"Delegation turned off for issue")
+ elseif area_id then
+ slot.put(_"Delegation turned off for area")
end
end
- }
- end
- ui.container{
- attr = { class = overridden and "delegation_overridden" or "" },
- content = function()
- execute.view{
- module = "member",
- view = "_show_thumb",
- params = { member = record }
- }
- end
- }
- if issue and issue.state ~= 'voting' and record.participation and not record.overridden then
- ui.container{
- attr = { class = "delegation_participation" },
- content = function()
- if i == #delegation_chain then
- ui.tag{ content = _"This member is currently participating in this issue." }
- else
- ui.tag{ content = _"This member is participating, the remaining delegation chain is suspended during discussing." }
- end
- end
- }
- end
- slot.put(" ")
- end )
-end
-
-if preview_trustee_id == 0 or not preview_trustee_id == null and delegation and not delegation.trustee_id then
- ui.image{
- static = "icons/16/table_go_crossed.png"
- }
- if issue_id then
- slot.put(_"Delegation turned off for issue")
- elseif area_id then
- slot.put(_"Delegation turned off for area")
- end
-end
+
+ end }
+
+ end }
+
+ end }
-end )
\ No newline at end of file
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/draft/_action/add.lua
--- a/app/main/draft/_action/add.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/draft/_action/add.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,8 +1,31 @@
+local draft_text = param.get("content")
+
+if not draft_text then
+ return false
+end
+
+local draft_text = util.wysihtml_preproc(draft_text)
+
+local valid_html, error_message = util.html_is_safe(draft_text)
+if not valid_html then
+ slot.put_into("error", _("Draft contains invalid formatting or character sequence: #{error_message}", { error_message = error_message }) )
+ return false
+end
+
+if config.initiative_abstract then
+ local abstract = param.get("abstract")
+ if not abstract then
+ return false
+ end
+ abstract = encode.html(abstract)
+ draft_text = abstract .. "" .. draft_text
+end
+
return Draft:update_content(
app.session.member.id,
param.get("initiative_id", atom.integer),
param.get("formatting_engine"),
- param.get("content"),
+ draft_text,
nil,
param.get("preview") or param.get("edit")
)
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/draft/_show.lua
--- a/app/main/draft/_show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/draft/_show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -10,7 +10,11 @@
content = draft.content
}
else
- slot.put(draft:get_content("html"))
+ if draft.formatting_engine == "html" or not draft.formatting_engine then
+ slot.put(draft.content)
+ else
+ slot.put(draft:get_content("html"))
+ end
end
end
}
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/draft/diff.lua
--- a/app/main/draft/diff.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/draft/diff.lua Sun Jul 15 14:07:29 2018 +0200
@@ -33,41 +33,8 @@
end
-execute.view{ module = "issue", view = "_sidebar_state", params = {
- initiative = initiative
-} }
-
-execute.view {
- module = "issue", view = "_sidebar_issue",
- params = {
- issue = initiative.issue,
- highlight_initiative_id = initiative.id
- }
-}
-
-execute.view {
- module = "issue", view = "_sidebar_whatcanido",
- params = { initiative = initiative }
-}
-
-execute.view {
- module = "issue", view = "_sidebar_members", params = {
- issue = initiative.issue, initiative = initiative
- }
-}
-
-
-
-execute.view {
- module = "issue", view = "_head", params = {
- issue = initiative.issue
- }
-}
-
-
-
-local old_draft_content = string.gsub(string.gsub(old_draft.content, "\n", " ###ENTER###\n"), " ", "\n")
-local new_draft_content = string.gsub(string.gsub(new_draft.content, "\n", " ###ENTER###\n"), " ", "\n")
+local old_draft_content = string.gsub(string.gsub(util.html_to_text(old_draft.content), "\n", " ###ENTER###\n"), " ", "\n")
+local new_draft_content = string.gsub(string.gsub(util.html_to_text(new_draft.content), "\n", " ###ENTER###\n"), " ", "\n")
local key = multirand.string(24, "0123456789abcdefghijklmnopqrstuvwxyz")
@@ -84,7 +51,7 @@
new_draft_file:write("\n")
new_draft_file:close()
-local output, err, status = extos.pfilter(nil, "sh", "-c", "diff -U 1000000000 '" .. old_draft_filename .. "' '" .. new_draft_filename .. "' | grep -v ^--- | grep -v ^+++ | grep -v ^@")
+local output, err, status = extos.pfilter(nil, "sh", "-c", "diff -a -U 1000000000 '" .. old_draft_filename .. "' '" .. new_draft_filename .. "' | grep --binary-files=text -v ^--- | grep --binary-files=text -v ^+++ | grep --binary-files=text -v ^@")
os.remove(old_draft_filename)
os.remove(new_draft_filename)
@@ -116,93 +83,133 @@
if not state_changed then
slot.put(" ")
end
- slot.put(encode.html(line))
+ --slot.put(encode.html(line))
+ slot.put(line)
else
slot.put(" ")
end
end
-ui.section( function()
- ui.sectionHead( function()
- ui.link{
- module = "initiative", view = "show", id = initiative.id,
- content = function ()
+execute.view{ module = "issue", view = "_head", params = { issue = initiative.issue } }
+
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function ()
ui.heading {
- level = 1,
- content = initiative.display_name
- }
- end
- }
- ui.heading{ level = 2, content = _("Comparision of revisions #{id1} and #{id2}", {
- id1 = old_draft.id,
- id2 = new_draft.id
- } ) }
- end )
-
- if app.session.member_id and not new_draft.initiative.revoked then
- local supporter = app.session.member:get_reference_selector("supporters")
- :add_where{ "initiative_id = ?", new_draft.initiative_id }
- :optional_object_mode()
- :exec()
- if supporter and supporter.draft_id ~= new_draft.id then
- ui.sectionRow("draft_updated_info", function()
- ui.container{
- attr = { class = "info" },
- content = _"The draft of this initiative has been updated!"
+ attr = { class = "mdl-card__title-text" },
+ content = function()
+ ui.link{
+ module = "initiative", view = "show", id = initiative.id,
+ content = initiative.display_name
+ }
+ end
}
- slot.put(" ")
- ui.link{
- text = _"refresh my support",
- module = "initiative",
- action = "add_support",
- id = new_draft.initiative.id,
- params = { draft_id = new_draft.id },
- routing = {
- default = {
- mode = "redirect",
- module = "initiative",
- view = "show",
- id = new_draft.initiative.id
- }
+ ui.container{ content = _("Comparision of revisions #{id1} and #{id2}", {
+ id1 = old_draft.id,
+ id2 = new_draft.id
+ } ) }
+ end }
+
+ if app.session.member_id and not new_draft.initiative.revoked then
+ local supporter = app.session.member:get_reference_selector("supporters")
+ :add_where{ "initiative_id = ?", new_draft.initiative_id }
+ :optional_object_mode()
+ :exec()
+ if supporter and supporter.draft_id == old_draft.id and new_draft.id == initiative.current_draft.id then
+ ui.container {
+ attr = { class = "mdl-card__content mdl-card--no-bottom-pad mdl-card--notice" },
+ content = _"The draft of this initiative has been updated!"
}
- }
+ ui.container {
+ attr = { class = "mdl-card__actions mdl-card--action-border mdl-card--notice" },
+ content = function ()
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised" },
+ text = _"refresh my support",
+ module = "initiative",
+ action = "add_support",
+ id = new_draft.initiative.id,
+ params = { draft_id = new_draft.id },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = "initiative",
+ view = "show",
+ id = new_draft.initiative.id
+ }
+ }
+ }
- slot.put(" · ")
-
- ui.link{
- text = _"remove my support",
- module = "initiative",
- action = "remove_support",
- id = new_draft.initiative.id,
- routing = {
- default = {
- mode = "redirect",
- module = "initiative",
- view = "show",
- id = new_draft.initiative.id
- }
+ slot.put(" ")
+
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised" },
+ text = _"remove my support",
+ module = "initiative",
+ action = "remove_support",
+ id = new_draft.initiative.id,
+ routing = {
+ default = {
+ mode = "redirect",
+ module = "initiative",
+ view = "show",
+ id = new_draft.initiative.id
+ }
+ }
+ }
+
+ slot.put(" ")
+
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button" },
+ text = _"cancel",
+ module = "initiative",
+ view = "show",
+ id = new_draft.initiative.id,
+ }
+ end
}
- }
-
- end )
- end
- end
-
- ui.sectionRow( function()
+ end
+ end
- if not status then
- ui.field.text{ value = _"The drafts do not differ" }
- else
- ui.container{
- tag = "div",
- attr = { class = "diff" },
- content = function()
- output = output:gsub("[^\n\r]+", function(line)
- process_line(line)
- end)
+ ui.container {
+ attr = { class = "draft mdl-card__content mdl-card--border" },
+ content = function ()
+ if not status then
+ ui.field.text{ value = _"The drafts do not differ" }
+ else
+ ui.container{
+ tag = "div",
+ attr = { class = "diff" },
+ content = function()
+ output = output:gsub("[^\n\r]+", function(line)
+ process_line(line)
+ end)
+ end
+ }
+ end
end
}
- end
+ end }
+ end }
+ ui.cell_sidebar{ content = function()
+
+ execute.view{ module = "issue", view = "_sidebar", params = {
+ initiative = initiative,
+ issue = initiative.issue
+ } }
- end )
-end )
+ execute.view {
+ module = "issue", view = "_sidebar_whatcanido",
+ params = { initiative = initiative }
+ }
+
+ execute.view {
+ module = "issue", view = "_sidebar_members", params = {
+ issue = initiative.issue, initiative = initiative
+ }
+ }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/draft/new.lua
--- a/app/main/draft/new.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/draft/new.lua Sun Jul 15 14:07:29 2018 +0200
@@ -13,25 +13,16 @@
return
end
-
-execute.view{
- module = "issue", view = "_head", params = {
- issue = initiative.issue,
- initiative = initiative
- }
-}
-
-execute.view {
- module = "issue", view = "_sidebar_issue",
- params = {
- issue = initiative.issue,
- }
-}
-
-
+local draft = initiative.current_draft
+if config.initiative_abstract then
+ draft.abstract = string.match(draft.content, "(.+)")
+ if draft.abstract then
+ draft.content = string.match(draft.content, "(.*)")
+ end
+end
ui.form{
- record = initiative.current_draft,
+ record = draft,
attr = { class = "vertical section" },
module = "draft",
action = "add",
@@ -46,103 +37,119 @@
},
content = function()
- ui.sectionHead( function()
- ui.heading { level = 1, content = initiative.display_name }
- end)
-
- if param.get("preview") then
- ui.sectionRow( function()
- ui.field.hidden{ name = "formatting_engine", value = param.get("formatting_engine") }
- ui.field.hidden{ name = "content", value = param.get("content") }
- local formatting_engine
- if config.enforce_formatting_engine then
- formatting_engine = config.enforce_formatting_engine
- else
- formatting_engine = param.get("formatting_engine")
- end
- ui.container{
- attr = { class = "draft" },
- content = function()
- slot.put(format.wiki_text(param.get("content"), formatting_engine))
- end
- }
+ ui.grid{ content = function()
+ ui.cell_main{ content = function()
+ ui.container{ attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = initiative.display_name }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ if param.get("preview") then
+ ui.sectionRow( function()
+ if config.initiative_abstract then
+ ui.field.hidden{ name = "abstract", value = param.get("abstract") }
+ ui.container{
+ attr = { class = "abstract" },
+ content = param.get("abstract")
+ }
+ slot.put(" ")
+ end
+ local draft_text = param.get("content")
+ local draft_text = util.wysihtml_preproc(draft_text)
+ ui.field.hidden{ name = "content", value = draft_text }
+ ui.container{
+ attr = { class = "draft" },
+ content = function()
+ slot.put(draft_text)
+ end
+ }
+ slot.put(" ")
- slot.put(" ")
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- class = "btn btn-default",
- value = _'Publish now'
- },
- content = ""
- }
- slot.put(" ")
- slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored",
+ value = _'Publish now'
+ },
+ content = ""
+ }
+ slot.put(" ")
+
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "edit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect",
+ value = _'Edit again'
+ },
+ content = ""
+ }
+ slot.put(" ")
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- name = "edit",
- class = "btn-link",
- value = _'Edit again'
- },
- content = ""
- }
- slot.put(" | ")
- ui.link{
- content = _"Cancel",
- module = "initiative",
- view = "show",
- id = initiative.id
- }
- end )
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" },
+ content = _"Cancel",
+ module = "initiative",
+ view = "show",
+ id = initiative.id
+ }
+ end )
- else
- ui.sectionRow( function()
- execute.view{ module = "initiative", view = "_sidebar_wikisyntax" }
-
- if not config.enforce_formatting_engine then
- ui.field.select{
- label = _"Wiki engine",
- name = "formatting_engine",
- foreign_records = config.formatting_engines,
- attr = {id = "formatting_engine"},
- foreign_id = "id",
- foreign_name = "name"
- }
- end
-
- ui.heading{ level = 2, content = _"Enter your proposal and/or reasons" }
+ else
+ ui.sectionRow( function()
+ if config.initiative_abstract then
+ ui.container { content = _"Enter abstract:" }
+ ui.container{ attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--expandable mdl-textfield__fullwidth" }, content = function()
+ ui.field.text{
+ name = "abstract",
+ multiline = true,
+ attr = { id = "abstract", style = "height: 20ex; width: 100%;" },
+ value = param.get("abstract")
+ }
+ end }
+ end
+
+ ui.container { content = _"Enter your proposal and/or reasons:" }
+ ui.field.wysihtml{
+ name = "content",
+ multiline = true,
+ attr = { id = "draft", style = "height: 50ex; width: 100%;" },
+ value = param.get("content")
+ }
+ if not issue or issue.state == "admission" or issue.state == "discussion" then
+ ui.container { content = _"You can change your text again anytime during admission and discussion phase" }
+ else
+ ui.container { content = _"You cannot change your text again later, because this issue is already in verfication phase!" }
+ end
+ slot.put(" ")
- ui.field.text{
- name = "content",
- multiline = true,
- attr = { style = "height: 50ex; width: 100%;" },
- value = param.get("content")
- }
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- name = "preview",
- class = "btn btn-default",
- value = _'Preview'
- },
- content = ""
- }
- slot.put(" ")
- slot.put(" ")
-
- ui.link{
- content = _"Cancel",
- module = "initiative",
- view = "show",
- id = initiative.id
- }
-
- end )
- end
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "preview",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored",
+ value = _'Preview'
+ },
+ content = ""
+ }
+ slot.put(" ")
+
+ ui.link{
+ content = _"Cancel",
+ module = "initiative",
+ view = "show",
+ id = initiative.id,
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" }
+ }
+
+ end )
+ end
+ end }
+ end }
+ end }
+ end }
end
}
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/draft/show.lua
--- a/app/main/draft/show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/draft/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,4 +1,12 @@
-local draft = Draft:new_selector():add_where{ "id = ?", param.get_id() }:single_object_mode():exec()
+local draft = Draft:new_selector():add_where{ "id = ?", param.get_id() }:optional_object_mode():exec()
+
+if not draft then
+ execute.view { module = "index", view = "404" }
+ request.set_status("404 Not Found")
+ return
+end
+
+
local source = param.get("source", atom.boolean)
execute.view{
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/help/introduction.lua
--- a/app/main/help/introduction.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/help/introduction.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,51 +1,36 @@
ui.title(_"Introduction")
--- show the user what can be done
-execute.view { module = "index", view = "_sidebar_whatcanido" }
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
-ui.section(function()
- ui.sectionHead(function()
- ui.heading{ level = 1, content = _"Structured discussion" }
- end)
- ui.sectionRow(function()
- ui.heading{ level = 2, content = _"Initiatives and issues" }
- ui.tag{ tag = "p", content = _"[introduction] iniatives and issues" }
- ui.heading{ level = 2, content = _"Subject areas" }
- ui.tag{ tag = "p", content = _"[introduction] subject areas" }
- ui.heading{ level = 2, content = _"Organizational units" }
- ui.tag{ tag = "p", content = _"[introduction] organizational units" }
- ui.heading{ level = 2, content = _"Rules of procedure" }
- ui.tag{ tag = "p", content = _"[introduction] rules of procedure" }
- end )
-end )
-ui.section(function()
- ui.sectionHead(function()
- ui.heading{ level = 1, content = _"4 phases of a decision" }
- end)
- ui.sectionRow(function()
- ui.heading{ level = 2, content = _"(1) Admission phase" }
- ui.tag{ tag = "p", content = _"[introduction] phase 1 admission" }
- ui.heading{ level = 2, content = _"(2) Discussion phase" }
- ui.tag{ tag = "p", content = _"[introduction] phase 2 discussion" }
- ui.heading{ level = 2, content = _"(3) Verification phase" }
- ui.tag{ tag = "p", content = _"[introduction] phase 3 verification" }
- ui.heading{ level = 2, content = _"(4) Voting phase" }
- ui.tag{ tag = "p", content = _"[introduction] phase 4 voting" }
- end)
-end)
-ui.section(function()
- ui.sectionHead(function()
- ui.heading{ level = 1, content = _"Vote delegation" }
- end)
- ui.sectionRow(function()
- ui.tag{ tag = "p", content = _"[introduction] vote delegation" }
- end)
-end)
-ui.section(function()
- ui.sectionHead(function()
- ui.heading{ level = 1, content = _"Preference voting" }
- end)
- ui.sectionRow(function()
- ui.tag{ tag = "p", content = _"[introduction] preference voting" }
- end)
-end)
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--has-fab mdl-card--border" }, content = function ()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 1, content = _"Quick guide" }
+ end }
+ ui.container { attr = { class = "draft mdl-card__content mdl-card--border" }, content = function()
+ ui.heading{ level = 2, content = _"Initiatives and issues" }
+ ui.tag{ tag = "p", content = _"[introduction] iniatives and issues" }
+ ui.heading{ level = 2, content = _"Subject areas" }
+ ui.tag{ tag = "p", content = _"[introduction] subject areas" }
+ ui.heading{ level = 2, content = _"Organizational units" }
+ ui.tag{ tag = "p", content = _"[introduction] organizational units" }
+ ui.heading{ level = 2, content = _"Rules of procedure" }
+ ui.tag{ tag = "p", content = _"[introduction] rules of procedure" }
+ ui.heading{ level = 2, content = _"Admission phase" }
+ ui.tag{ tag = "p", content = _"[introduction] phase 1 admission" }
+ ui.heading{ level = 2, content = _"Discussion phase" }
+ ui.tag{ tag = "p", content = _"[introduction] phase 2 discussion" }
+ ui.heading{ level = 2, content = _"Verification phase" }
+ ui.tag{ tag = "p", content = _"[introduction] phase 3 verification" }
+ ui.heading{ level = 2, content = _"Voting phase" }
+ ui.tag{ tag = "p", content = _"[introduction] phase 4 voting" }
+ ui.heading{ level = 2, content = _"Vote delegation" }
+ ui.tag{ tag = "p", content = _"[introduction] vote delegation" }
+ ui.heading{ level = 2, content = _"Preference voting" }
+ ui.tag{ tag = "p", content = _"[introduction] preference voting" }
+
+ end }
+ end }
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/http_options.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/http_options.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,17 @@
+-- TODO workaround, needs to be resolved in WebMCP's request.handler
+if not request._route then
+ return
+end
+
+if request.get_module() == "oauth2" and request.get_view() == "session" then
+ local origin = request.get_header("Origin")
+ if origin then
+ request.add_header("Access-Control-Allow-Origin", origin)
+ end
+ request.add_header("Access-Control-Allow-Credentials", "true")
+ request.add_header("Access-Control-Max-Age", "0")
+else
+ request.add_header("Access-Control-Allow-Origin", "*")
+end
+
+request.add_header("Access-Control-Allow-Headers", "Authorization")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/403.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/index/403.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,21 @@
+request.set_status("403 Forbidden")
+
+ui.title("403 Forbidden")
+
+ui.grid{ content = function()
+
+ ui.cell_main{ content = function()
+
+ ui.container { attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Forbidden" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ ui.link{
+ content = _"Go back to home page",
+ module = "index", view = "index"
+ }
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/404.lua
--- a/app/main/index/404.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/404.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,13 +1,21 @@
+request.set_status("404 Not found")
+
ui.title("404 Not found")
-ui.section(function()
- ui.sectionHead(function()
- ui.heading{ level = 1, content = _"Page not found" }
- end)
- ui.sectionRow(function()
- ui.link{
- content = _"Go back to home page",
- module = "index", view = "index"
- }
- end)
-end)
\ No newline at end of file
+ui.grid{ content = function()
+
+ ui.cell_main{ content = function()
+
+ ui.container { attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Page not found" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ ui.link{
+ content = _"Go back to home page",
+ module = "index", view = "index"
+ }
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/405.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/index/405.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,21 @@
+request.set_status("405 Method Not Allowed")
+
+ui.title("405 Method Not Allowed")
+
+ui.grid{ content = function()
+
+ ui.cell_main{ content = function()
+
+ ui.container { attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Method not allowed" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ ui.link{
+ content = _"Go back to home page",
+ module = "index", view = "index"
+ }
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_action/login.lua
--- a/app/main/index/_action/login.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_action/login.lua Sun Jul 15 14:07:29 2018 +0200
@@ -99,6 +99,7 @@
member:save()
app.session.member = member
app.session:save()
+
trace.debug('User authenticated')
if config.etherpad then
do_etherpad_auth(member)
@@ -116,9 +117,7 @@
ui.tag { content = _"to show more info and learn what you can do" }
end )
else
- slot.select("error", function()
- ui.tag{ content = _'Invalid login name or password!' }
- end)
+ slot.put_into("error_code", "invalid_credentials")
trace.debug('User NOT authenticated')
return false
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_action/logout.lua
--- a/app/main/index/_action/logout.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_action/logout.lua Sun Jul 15 14:07:29 2018 +0200
@@ -9,3 +9,7 @@
}
end
end
+
+if config.meta_navigation_logout_url then
+ request.redirect{ external = config.meta_navigation_logout_url }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_action/register.lua
--- a/app/main/index/_action/register.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_action/register.lua Sun Jul 15 14:07:29 2018 +0200
@@ -4,13 +4,13 @@
if app.session.authority == "ldap" then
if not config.ldap.member or config.ldap.member.registration ~= "manual" then
- error("access denied")
+ return execute.view { module = "index", view = "403" }
end
member = ldap.create_member(app.session.authority_uid, true)
else
if config.registration_disabled then
- error("registration disabled")
+ return execute.view { module = "index", view = "403" }
end
member = Member:new_selector()
:add_where{ "invite_code = ?", code }
@@ -21,38 +21,58 @@
:exec()
end
-
+
if not member then
slot.put_into("error", _"The code you've entered is invalid")
request.redirect{
mode = "forward",
module = "index",
- view = "register"
+ view = "register", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
}
return false
end
local notify_email = param.get("notify_email")
-if not util.is_profile_field_locked(member, "notify_email") and notify_email then
+if not util.is_profile_field_locked(member, "notify_email") and not member.notify_email and notify_email then
if #notify_email < 5 then
slot.put_into("error", _"Email address too short!")
request.redirect{
mode = "redirect",
module = "index",
view = "register",
- params = { code = member.invite_code }
+ params = {
+ code = member.invite_code,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
}
return false
end
end
-if member and not util.is_profile_field_locked(member, "notify_email") and not notify_email then
+if member and not util.is_profile_field_locked(member, "notify_email") and not member.notify_email and not notify_email then
request.redirect{
mode = "redirect",
module = "index",
view = "register",
- params = { code = member.invite_code, step = 1 }
+ params = {
+ code = member.invite_code,
+ skip = param.get("skip"),
+ step = 1,
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
}
return false
end
@@ -71,7 +91,12 @@
params = {
code = member.invite_code,
notify_email = notify_email,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -87,7 +112,12 @@
params = {
code = member.invite_code,
notify_email = notify_email,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -105,7 +135,12 @@
params = {
code = member.invite_code,
notify_email = notify_email,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -124,7 +159,12 @@
code = member.invite_code,
notify_email = notify_email,
name = member.name,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -141,7 +181,12 @@
code = member.invite_code,
notify_email = notify_email,
name = member.name,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -158,7 +203,12 @@
code = member.invite_code,
notify_email = notify_email,
name = member.name,
- step = 1
+ step = 1,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
return false
@@ -169,10 +219,16 @@
if step > 2 then
for i, checkbox in ipairs(config.use_terms_checkboxes) do
- local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
- if not accepted then
- slot.put_into("error", checkbox.not_accepted_error)
- return false
+ local member_useterms = MemberUseterms:new_selector()
+ :add_where{ "member_id = ?", member.id }
+ :add_where{ "contract_identifier = ?", checkbox.name }
+ :exec()
+ if #member_useterms == 0 then
+ local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
+ if not accepted then
+ slot.put_into("error", checkbox.not_accepted_error)
+ return false
+ end
end
end
@@ -190,7 +246,12 @@
code = member.invite_code,
notify_email = notify_email,
name = member.name,
- login = member.login
+ login = member.login,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
--]]
@@ -219,7 +280,7 @@
member.name = name
end
- if notify_email ~= member.notify_email then
+ if not member.notify_email then
local success = member:set_notify_email(notify_email)
if not success then
slot.put_into("error", _"Can't send confirmation email")
@@ -231,7 +292,10 @@
for i, checkbox in ipairs(config.use_terms_checkboxes) do
local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
- member:set_setting("use_terms_checkbox_" .. checkbox.name, "accepted at " .. tostring(now))
+ local member_useterms = MemberUseterms:new()
+ member_useterms.member_id = member.id
+ member_useterms.contract_identifier = checkbox.name
+ member_useterms:save()
end
member.activated = 'now'
@@ -239,12 +303,24 @@
member.last_activity = 'now'
member:save()
- slot.put_into("notice", _"You've successfully registered and you can login now with your login and password!")
+ if not member.profile then
+ local profile = MemberProfile:new()
+ profile.member_id = member.id
+ profile.profile = json.object()
+ profile:save()
+ end
+
+ slot.put_into("notice", _"Registration succeeded")
+
+ app.session.member_id = member.id
+ app.session:save()
request.redirect{
mode = "redirect",
- module = "index",
- view = "login",
+ module = param.get("redirect_module") or "index",
+ view = param.get("redirect_view") or "index",
+ id = param.get("redirect_id"),
+ params = param.get("redirect_params")
}
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_drawer.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/index/_drawer.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,141 @@
+local member = app.session.member
+local units
+if member then
+ units = member:get_reference_selector("units"):add_order_by("name"):exec()
+ units:load_delegation_info_once_for_member_id(member.id)
+else
+ units = Unit:new_selector():add_where("active"):add_order_by("name"):exec()
+end
+
+slot.select("drawer", function()
+ ui.container{ attr = { class = "mdl-layout__drawer" }, content = function()
+ ui.tag{ tag = "nav", attr = { class = "mdl-navigation" }, content = function ()
+ ui.link{ content = config.instance_name, attr = { class = "mdl-navigation__link mdl-menu__item--full-bleed-divider mdl-navigation__head" }, module = "index", view = "index" }
+
+ if #units > 0 then
+ for i, unit in ipairs(units) do
+ local class = "mdl-navigation__link mdl-navigation__head"
+ if i == #units then
+ class = class .. " mdl-menu__item--full-bleed-divider"
+ end
+ ui.link{ attr = { class = class }, content = unit.name, module = "unit", view = "show", id = unit.id }
+ local areas = unit.areas
+ for i, area in ipairs(areas) do
+ local class = "mdl-navigation__link mdl-menu__item--small"
+ if i == #areas then
+ class = class .. " mdl-menu__item--full-bleed-divider"
+ end
+ ui.link{ attr = { class = class }, module = "area", view = "show", id = area.id, content = function()
+ ui.tag{ content = "⤷" }
+ slot.put(" ")
+ ui.tag{ content = area.name }
+ end }
+ end
+ end
+ end
+
+ ui.link{ attr = { class = "mdl-navigation__link mdl-menu__item--full-bleed-divider" }, content = _"Member list", module = "member", view = "list" }
+ ui.link{ attr = { class = "mdl-navigation__link" }, content = _"Quick guide", module = "help", view = "introduction" }
+
+ if app.session.member_id and app.session.member.admin then
+ ui.link{ attr = { class = "mdl-navigation__link mdl-menu__item--full-bleed-divider" }, content = _"System settings", module = "admin", view = "index" }
+ end
+
+ end }
+ end }
+end)
+
+if true then
+ return
+end
+
+for i, unit in ipairs(units) do
+
+ ui.sidebar ( "tab-whatcanido units", function ()
+
+ local areas_selector = Area:new_selector()
+ :reset_fields()
+ :add_field("area.id", nil, { "grouped" })
+ :add_field("area.unit_id", nil, { "grouped" })
+ :add_field("area.name", nil, { "grouped" })
+ :add_where{ "area.unit_id = ?", unit.id }
+ :add_where{ "area.active" }
+ :add_order_by("area.name")
+
+ local areas = areas_selector:exec()
+ if member then
+ areas:load_delegation_info_once_for_member_id(member.id)
+ end
+
+ if #areas > 0 then
+
+ ui.container {
+ attr = { class = "sidebarHead" },
+ content = function ()
+ ui.heading { level = 2, content = function ()
+ ui.link {
+ attr = { class = "unit" },
+ module = "unit", view = "show", id = unit.id,
+ content = unit.name
+ }
+
+ if member then
+ local delegation = Delegation:by_pk(member.id, unit.id, nil, nil)
+
+ if delegation then
+ ui.link {
+ module = "delegation", view = "show", params = {
+ unit_id = unit.id
+ },
+ attr = { class = "delegation_info" },
+ content = function ()
+ ui.delegation(delegation.trustee_id, delegation.trustee.name)
+ end
+ }
+ end
+ end
+ end }
+
+ end
+ }
+
+
+ ui.tag { tag = "div", attr = { class = "areas areas-" .. unit.id }, content = function ()
+
+ for i, area in ipairs(areas) do
+
+ ui.tag { tag = "div", attr = { class = "sidebarRow" }, content = function ()
+
+ if member then
+ local delegation = Delegation:by_pk(member.id, nil, area.id, nil)
+
+ if delegation then
+ ui.link {
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ attr = { class = "delegation_info" },
+ content = function ()
+ ui.delegation(delegation.trustee_id, delegation.trustee_id and delegation.trustee.name)
+ end
+ }
+ end
+ end
+
+ slot.put ( " " )
+
+ ui.link {
+ attr = { class = "area" },
+ module = "area", view = "show", id = area.id,
+ content = area.name
+ }
+
+
+ end }
+ end
+ end }
+ end
+ end )
+end
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_head.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/index/_head.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,142 @@
+local unit_id = config.single_unit_id or tonumber(request.get_param{ name = "unit" })
+if unit_id == "all" then
+ unit_id = nil
+end
+local unit
+if unit_id then
+ unit = Unit:by_id(unit_id)
+end
+local area_id = config.single_area_id or tonumber(request.get_param{ name = "area" })
+if area_id == "all" then
+ area_id = nil
+end
+local area
+if area_id then
+ area = Area:by_id(area_id)
+end
+if area and unit and area.unit_id ~= unit.id then
+ area_id = nil
+end
+if area and area.unit_id == unit_id then
+ if app.session.member_id then
+ area:load_delegation_info_once_for_member_id(app.session.member_id)
+ end
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ if unit then
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ if not config.single_area_id then
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = unit.name .. " / " .. area.name }
+ else
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = unit.name }
+ end
+ end }
+ end
+ if area.description and #(area.description) > 0 then
+ if not config.single_area_id then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = area.description }
+ else
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = unit.description }
+ end
+ end
+ if not (config.voting_only and config.disable_delegations) and app.session.member_id then
+ ui.container{ attr = { class = "mdl-card__actions" }, content = function()
+
+ if app.session.member_id then
+ if area.delegation_info.first_trustee_id then
+ local member = Member:by_id(area.delegation_info.first_trustee_id)
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "forward" }
+ execute.view{
+ module = "member_image",
+ view = "_show",
+ params = {
+ member = member,
+ image_type = "avatar",
+ show_dummy = true
+ }
+ }
+ end
+
+ if not config.disable_delegations then
+ if area.delegation_info.own_delegation_scope == "unit" then
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "delegation", view = "show", params = {
+ unit_id = area.unit_id,
+ },
+ content = _("change/revoke delegation of organizational unit")
+ }
+ slot.put(" ")
+ end
+ if area.delegation_info.own_delegation_scope == nil then
+ local text
+ if config.single_area_id then
+ text = _"choose delegatee"
+ else
+ text = _"choose subject area delegatee"
+ end
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "forward" }
+ ui.tag{ content = text }
+ end
+ }
+ slot.put(" ")
+ elseif area.delegation_info.own_delegation_scope == "area" then
+ local text
+ if config.single_area_id then
+ text = _"change/revoke delegation"
+ else
+ text = _"change/revoke area delegation"
+ end
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = text
+ }
+ slot.put(" ")
+ else
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "delegation", view = "show", params = {
+ area_id = area.id
+ },
+ content = _"change/revoke delegation only for this subject area"
+ }
+ slot.put(" ")
+ end
+ end
+ end
+ if not config.voting_only and app.session.member_id and app.session.member:has_initiative_right_for_unit_id ( area.unit_id ) then
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "initiative", view = "new",
+ params = { area_id = area.id },
+ content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "add" }
+ ui.tag{ content = _"new issue" }
+ end
+ }
+ end
+ end }
+ end
+ end }
+elseif unit then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = unit.name }
+ end }
+ if unit.description and #(unit.description) > 0 then
+ ui.container{ attr = { class = "mdl-card__supporting-text mdl-card--border" }, content = unit.description }
+ end
+ --ui.container{ attr = { class = "mdl-card__actions mdl-card--border" }, content = function()
+ --end }
+ end }
+end
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_lang_chooser.lua
--- a/app/main/index/_lang_chooser.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_lang_chooser.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,31 @@
+ui.tag{ tag = "button", attr = { id = "lf-lang-menu", class = "mdl-button mdl-js-button float-right" }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "language" }
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "translate" }
+end }
+
+ui.tag { tag = "ul", attr = { class = "mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect", ["data-mdl-for"] = "lf-lang-menu" }, content = function()
+ for i, lang in ipairs(config.enabled_languages) do
+ local langcode
+ locale.do_with({ lang = lang }, function()
+ langcode = _("[Name of Language]")
+ end)
+ ui.tag{ tag = "li", attr = { class = "mdl-menu__item" }, content = function()
+ ui.link{
+ content = langcode,
+ attr = { class = "mdl-menu__link" },
+ module = "index",
+ action = "set_lang",
+ params = { lang = lang },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = request.get_module(),
+ view = request.get_view(),
+ id = request.get_id_string(),
+ params = request.get_param_strings()
+ }
+ }
+ }
+ end }
+ end
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_members.lua
--- a/app/main/index/_sidebar_members.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_members.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,38 +1,60 @@
+local limit = 50
+
if not app.session:has_access("all_pseudonymous") then
return
end
-local member_count = MemberCount:get()
+local unit_id = request.get_param{ name = "unit" }
+if unit_id == "all" then
+ unit_id = nil
+end
-if not member_count then
- return
+local selector = Member:new_selector()
+ :add_where("active")
+ :add_order_by("last_login DESC NULLS LAST, id DESC")
+
+if unit_id then
+ selector:join("privilege", nil, "privilege.member_id = member.id")
+ selector:add_where{ "privilege.unit_id = ?", unit_id }
end
-ui.sidebar ( "tab-members", function ()
- ui.sidebarHead( function()
- ui.heading {
- level = 2,
- content = _("Registered members (#{count})", { count = member_count })
+local member_count = selector:count()
+
+selector:limit(limit)
+
+
+ui.container{ attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ local text
+ if unit_id then
+ text = _("Eligible members (#{count})", { count = selector:count() })
+ else
+ text = _("Registered members (#{count})", { count = selector:count() })
+ end
+ ui.container{
+ attr = { class = "mdl-card__title-text" },
+ content = text
}
- end )
-
- local selector = Member:new_selector()
- :add_where("active")
- :add_order_by("last_login DESC NULLS LAST, id DESC")
- :limit(50)
+ end }
- execute.view {
- module = 'member', view = '_list', params = {
- members_selector = selector,
- no_filter = true, no_paginate = true,
- member_class = "sidebarRow sidebarRowNarrow"
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ execute.view {
+ module = 'member', view = '_list', params = {
+ members_selector = selector,
+ no_filter = true, no_paginate = true,
+ member_class = "sidebarRow sidebarRowNarrow"
+ }
}
- }
+ end }
- ui.link {
- attr = { class = "sidebarRow moreLink" },
- text = _"Show full member list",
- module = "member", view = "list"
- }
-
-end )
+ if member_count > limit then
+ ui.container{ attr = { class = "mdl-card__actions mdl-card--border" }, content = function()
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button" },
+ text = _"Show full member list",
+ module = "member", view = "list"
+ }
+ end }
+ end
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_motd.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/index/_sidebar_motd.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,26 @@
+if app.session.member and config.motd_intern or config.motd_extern then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Message of the day" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content what-can-i-do-here" }, content = function()
+ if app.session.member and config.motd_intern then
+ ui.container{
+ attr = { class = "draft motd" },
+ content = function()
+ slot.put(config.motd_intern)
+ end
+ }
+ end
+ if config.motd_extern then
+ ui.container{
+ attr = { class = "draft motd" },
+ content = function()
+ slot.put(config.motd_extern)
+ end
+ }
+ end
+ end }
+ end }
+ slot.put(" ")
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_motd_intern.lua
--- a/app/main/index/_sidebar_motd_intern.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_motd_intern.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,10 +1,15 @@
if config.motd_intern then
- slot.select("motd", function()
- ui.container{
- attr = { class = "wiki motd" },
- content = function()
- slot.put(config.motd_intern)
- end
- }
- end )
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Message of the day" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content what-can-i-do-here" }, content = function()
+ ui.container{
+ attr = { class = "wiki motd" },
+ content = function()
+ slot.put(config.motd_intern)
+ end
+ }
+ end }
+ end }
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_motd_public.lua
--- a/app/main/index/_sidebar_motd_public.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_motd_public.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,10 +1,7 @@
if config.motd_public then
- slot.select("motd", function()
- ui.container{
- attr = { class = "wiki motd" },
- content = function()
- slot.put(config.motd_public)
- end
- }
- end )
+ ui.container{ attr = { class = "mdl-special-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ slot.put(config.motd_public)
+ end }
+ end }
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_notifications.lua
--- a/app/main/index/_sidebar_notifications.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_notifications.lua Sun Jul 15 14:07:29 2018 +0200
@@ -7,6 +7,18 @@
}
end
+local agents = Agent:new_selector()
+ :add_where{ "controller_id = ?", app.session.member_id }
+ :add_where{ "accepted ISNULL" }
+ :exec()
+for i, agent in ipairs(agents) do
+ local member = Member:by_id(agent.controlled_id)
+ notification_links[#notification_links+1] = {
+ module = "agent", view = "show", params = { controlled_id = agent.controlled_id },
+ text = _("Account access invitation from '#{member_name}'", { member_name = member.name })
+ }
+end
+
if config.check_delegations_interval_soft then
local member = Member:new_selector()
:add_where({ "id = ?", app.session.member_id })
@@ -99,28 +111,19 @@
}
end
-local mode = param.get("mode") or "view"
-
if #notification_links > 0 then
- if mode == "link" then
- slot.select("notification", function ()
- local text = _"notifications"
- ui.link {
- attr = { class = "notifications", title = text },
- module = "index", view = "notifications",
- content = function ()
- ui.image { attr = { class = "icon", alt = text }, static = "icons/48/bell.png" }
- ui.tag { attr = { class = "count" }, content = #notification_links }
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Notifications" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content what-can-i-do-here" }, content = function()
+ ui.tag{ tag = "ul", attr = { class = "ul" }, content = function()
+ for i, notification_link in ipairs(notification_links) do
+ ui.tag{ tag = "li", content = function()
+ ui.link(notification_link)
+ end }
end
- }
- end )
- elseif mode == "view" then
- ui.tag{ tag = "ul", attr = { class = "ul" }, content = function()
- for i, notification_link in ipairs(notification_links) do
- ui.tag{ tag = "li", content = function()
- ui.link(notification_link)
- end }
- end
+ end }
end }
- end
+ end }
end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_units.lua
--- a/app/main/index/_sidebar_units.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_units.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,3 +1,5 @@
+if true then return end
+
local member = param.get ( "member", "table" )
local units
if member then
@@ -30,14 +32,6 @@
:add_where{ "area.active" }
:add_order_by("area.name")
- if member then
- areas_selector:left_join (
- "membership", nil,
- { "membership.area_id = area.id AND membership.member_id = ?", member.id }
- )
- areas_selector:add_field("membership.member_id NOTNULL", "subscribed", { "grouped" })
- end
-
local areas = areas_selector:exec()
if member then
areas:load_delegation_info_once_for_member_id(member.id)
@@ -78,21 +72,9 @@
ui.tag { tag = "div", attr = { class = "areas areas-" .. unit.id }, content = function ()
- local any_subscribed = false
- local subscribed_count = 0
for i, area in ipairs(areas) do
- local class = "sidebarRow"
- class = class .. (not area.subscribed and " disabled" or "")
-
- ui.tag { tag = "div", attr = { class = class }, content = function ()
-
- if area.subscribed then
- local text = _"subscribed"
- ui.image { attr = { class = "icon16 star", alt = text, title = text }, static = "icons/48/star.png" }
- any_subscribed = true
- subscribed_count = subscribed_count +1
- end
+ ui.tag { tag = "div", attr = { class = "sidebarRow" }, content = function ()
if member then
local delegation = Delegation:by_pk(member.id, nil, area.id, nil)
@@ -121,31 +103,6 @@
end }
end
- if subscribed_count < #areas then
- local text
- if any_subscribed then
- text = _"show other subject areas"
- else
- text = _"show subject areas"
- end
- ui.script{ script = "$('.areas-" .. unit.id .. "').addClass('folded');" }
- ui.tag { tag = "div", attr = { class = "sidebarRow moreLink whenfolded" }, content = function ()
- ui.link {
- attr = {
- onclick = "$('.areas-" .. unit.id .. "').removeClass('folded'); return false;"
- },
- text = text
- }
- end }
- ui.tag { tag = "div", attr = { class = "sidebarRow moreLink whenunfolded" }, content = function ()
- ui.link {
- attr = {
- onclick = "$('.areas-" .. unit.id .. "').addClass('folded'); return false;"
- },
- text = _"collapse subject areas"
- }
- end }
- end
end }
end
end )
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/_sidebar_whatcanido.lua
--- a/app/main/index/_sidebar_whatcanido.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/_sidebar_whatcanido.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,84 +1,116 @@
-ui.sidebar ( "tab-whatcanido", function ()
-
- ui.sidebarHeadWhatCanIDo()
+ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"What can I do here?" }
+ end }
+ ui.container{ attr = { class = "what-can-i-do-here" }, content = function()
- if app.session.member then
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to know whats going on" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = _"take a look on the issues (see left)" }
- ui.tag { tag = "li", content = _"by default only those issues are shown, for which your are eligible to participate (change filters on top of the list)" }
- end }
- end )
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to stay informed" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.tag{ content = _"check your " }
- ui.link{
- module = "member", view = "settings_notification",
- params = { return_to = "home" },
- text = _"notifications settings"
- }
- end }
- ui.tag { tag = "li", content = function ()
- ui.tag{ content = _"subscribe subject areas or add your interested to issues and you will be notified about changes (follow the instruction on the area or issue page)" }
- end }
- end }
- end )
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to start a new initiative" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = _"open the appropriate subject area for your issue and follow the instruction on that page." }
- end }
- end )
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to delegate my vote" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = _"open the organizational unit, subject area or issue you like to delegate and follow the instruction on that page." }
- end }
- end )
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to take a look at other organizational units" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.link{
- module = "unit", view = "list",
- text = _"show all units"
- }
- end }
- end }
- end )
- if config.download_dir then
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to download all data" }
+ if app.session.member then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to know whats going on" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"take a look on the issues (see right)" }
+ ui.tag { tag = "li", content = _"by default only those issues are shown, for which your are eligible to participate (change filters on top of the list)" }
+ end }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to stay informed" }
ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
ui.tag { tag = "li", content = function ()
+ ui.tag{ content = _"check your " }
ui.link{
- module = "index", view = "download",
- text = _"download database"
+ module = "member", view = "settings_notification",
+ params = { return_to = "home" },
+ text = _"notifications settings"
}
end }
+ if not config.voting_only then
+ ui.tag { tag = "li", content = function ()
+ ui.tag{ content = _"subscribe subject areas or add your interested to issues and you will be notified about changes (follow the instruction on the area or issue page)" }
+ end }
+ end
+ end }
+ end }
+ if app.session.member.has_voting_right then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to vote" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"check the issues on the right, and click on 'Vote now' to vote on an issue which is in voting phase." }
+ end }
+ end }
+ if not config.disable_delegations then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to delegate my vote" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"open the organizational unit, subject area or issue you like to delegate and follow the instruction on that page." }
+ end }
+ end }
+ end
+ end
+ if not config.voting_only and app.session.member.has_initiative_right then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to start a new initiative" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"open the appropriate subject area for your issue and follow the instruction on that page." }
+ end }
+ end }
+ end
+ if not config.single_unit_id then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to take a look at other organizational units" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.link{
+ module = "unit", view = "list",
+ text = _"show all units"
+ }
+ end }
+ end }
+ end }
+ end
+ if config.download_dir then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to download all data" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.link{
+ module = "index", view = "download",
+ text = _"download database"
+ }
+ end }
+ end }
+ end }
+ end
+ end
+ if not app.session.member then
+ ui.container { attr = { class = "mdl-card__content mdl-card--border" }, content = function ()
+ ui.tag{ content = _"You are not entitled to vote in this unit" }
+ ui.tag{ tag = "ul", content = function()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "index", view = "login", content = _"Login" }
+ end }
+ end }
+ end }
+ end
+ if not config.voting_only then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to learn more about LiquidFeedback" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function()
+ ui.link { module = "help", view = "introduction", content = _"structured discussion" }
+ end }
+ ui.tag { tag = "li", content = function()
+ ui.link { module = "help", view = "introduction", content = _"4 phases of a decision" }
+ end }
+ if not config.disable_delegations then
+ ui.tag { tag = "li", content = function()
+ ui.link { module = "help", view = "introduction", content = _"vote delegation" }
+ end }
+ end
+ ui.tag { tag = "li", content = function()
+ ui.link { module = "help", view = "introduction", content = _"preference voting" }
+ end }
end }
- end )
+ end }
end
- end
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to learn more about LiquidFeedback" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function()
- ui.link { module = "help", view = "introduction", content = _"structured discussion" }
- end }
- ui.tag { tag = "li", content = function()
- ui.link { module = "help", view = "introduction", content = _"4 phases of a decision" }
- end }
- ui.tag { tag = "li", content = function()
- ui.link { module = "help", view = "introduction", content = _"vote delegation" }
- end }
- ui.tag { tag = "li", content = function()
- ui.link { module = "help", view = "introduction", content = _"preference voting" }
- end }
- end }
- end )
-
-end )
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/about.lua
--- a/app/main/index/about.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/about.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,103 +1,104 @@
ui.title(_"About site")
-ui.section( function()
+ui.grid{ content = function()
+ ui.cell_full{ content = function()
- ui.sectionHead( function()
- ui.heading{ level = 1, content = _"About site" }
- end )
-
- ui.sectionRow( function()
+ ui.container { attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
- ui.heading{ level = 3, content = _"This service is provided by:" }
- slot.put(config.app_service_provider)
-
- end )
-
- ui.sectionRow( function()
-
- ui.heading{ level = 3, content = _"This service is provided using the following software components:" }
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"This service is provided by:" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ slot.put(config.app_service_provider)
+ end }
+ end }
+
+ ui.container { attr = { class = "mdl-card mdl-shadow--2dp mdl-card__fullwidth" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"This service is provided using the following software components:" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
- local tmp = {
- {
- name = "LiquidFeedback Frontend",
- url = "http://www.public-software-group.org/liquid_feedback",
- version = config.app_version,
- license = "MIT/X11",
- license_url = "http://www.public-software-group.org/licenses"
- },
- {
- name = "LiquidFeedback Core",
- url = "http://www.public-software-group.org/liquid_feedback",
- version = db:query("SELECT * from liquid_feedback_version;")[1].string,
- license = "MIT/X11",
- license_url = "http://www.public-software-group.org/licenses"
- },
- {
- name = "WebMCP",
- url = "http://www.public-software-group.org/webmcp",
- version = WEBMCP_VERSION or _WEBMCP_VERSION,
- license = "MIT/X11",
- license_url = "http://www.public-software-group.org/licenses"
- }
- }
-
- if _MOONBRIDGE_VERSION then
- tmp[#tmp+1] = {
- name = "Moonbridge",
- url = "http://www.public-software-group.org/moonbridge",
- version = _MOONBRIDGE_VERSION,
- license = "MIT/X11",
- license_url = "http://www.public-software-group.org/licenses"
- }
- end
-
- tmp[#tmp+1] = {
- name = "Lua",
- url = "http://www.lua.org",
- version = _VERSION:gsub("Lua ", ""),
- license = "MIT/X11",
- license_url = "http://www.lua.org/license.html"
- }
-
- tmp[#tmp+1] = {
- name = "PostgreSQL",
- url = "http://www.postgresql.org/",
- version = db:query("SELECT version();")[1].version:gsub("PostgreSQL ", ""):gsub("on.*", ""),
- license = "PostgreSQL License",
- license_url = "http://www.postgresql.org/about/licence"
- }
+ local tmp = {
+ {
+ name = "LiquidFeedback Frontend",
+ url = "http://www.public-software-group.org/liquid_feedback",
+ version = config.app_version,
+ license = "MIT/X11",
+ license_url = "http://www.public-software-group.org/licenses"
+ },
+ {
+ name = "LiquidFeedback Core",
+ url = "http://www.public-software-group.org/liquid_feedback",
+ version = db:query("SELECT * from liquid_feedback_version;")[1].string,
+ license = "MIT/X11",
+ license_url = "http://www.public-software-group.org/licenses"
+ },
+ {
+ name = "WebMCP",
+ url = "http://www.public-software-group.org/webmcp",
+ version = WEBMCP_VERSION or _WEBMCP_VERSION,
+ license = "MIT/X11",
+ license_url = "http://www.public-software-group.org/licenses"
+ }
+ }
+
+ if _MOONBRIDGE_VERSION then
+ tmp[#tmp+1] = {
+ name = "Moonbridge",
+ url = "http://www.public-software-group.org/moonbridge",
+ version = _MOONBRIDGE_VERSION,
+ license = "MIT/X11",
+ license_url = "http://www.public-software-group.org/licenses"
+ }
+ end
+
+ tmp[#tmp+1] = {
+ name = "Lua",
+ url = "http://www.lua.org",
+ version = _VERSION:gsub("Lua ", ""),
+ license = "MIT/X11",
+ license_url = "http://www.lua.org/license.html"
+ }
+
+ tmp[#tmp+1] = {
+ name = "PostgreSQL",
+ url = "http://www.postgresql.org/",
+ version = db:query("SELECT version();")[1].version:gsub("PostgreSQL ", ""):gsub("on.*", ""),
+ license = "PostgreSQL License",
+ license_url = "http://www.postgresql.org/about/licence"
+ }
- ui.list{
- records = tmp,
- columns = {
- {
- content = function(record)
- ui.link{
- content = record.name,
- external = record.url
+ ui.list{
+ records = tmp,
+ columns = {
+ {
+ content = function(record)
+ ui.link{
+ content = record.name,
+ external = record.url
+ }
+ end
+ },
+ {
+ content = function(record) ui.field.text{ value = record.version } end
+ },
+ {
+ content = function(record)
+ ui.link{
+ content = record.license,
+ external = record.license_url
+ }
+ end
+
}
- end
- },
- {
- content = function(record) ui.field.text{ value = record.version } end
- },
- {
- content = function(record)
- ui.link{
- content = record.license,
- external = record.license_url
- }
- end
+ }
+ }
- }
- }
- }
-
- end )
-
- ui.sectionRow( function()
- ui.heading{ level = 3, content = "3rd party license information:" }
- slot.put('Some of the icons used in Liquid Feedback are from Silk icon set 1.3 by Mark James. His work is licensed under a Creative Commons Attribution 2.5 License.')
-
- end )
-end )
+ slot.put(" ")
+ ui.container{ content = "3rd party license information:" }
+ slot.put('Some of the icons used in Liquid Feedback are from Silk icon set 1.3 by Mark James. His work is licensed under a Creative Commons Attribution 2.5 License.')
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/document.lua
--- a/app/main/index/document.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/document.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,5 @@
if not config.document_dir then
- error("feature not enabled")
+ return execute.view { module = "index", view = "404" }
end
slot.put_into("title", _"Download documents")
@@ -54,4 +54,4 @@
end
}
}
-}
\ No newline at end of file
+}
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/document_file.lua
--- a/app/main/index/document_file.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/document_file.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,5 @@
if not config.document_dir then
- error("feature not enabled")
+ return execute.view { module = "index", view = "404" }
end
local filename = param.get("filename")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/download.lua
--- a/app/main/index/download.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/download.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,5 @@
if not config.download_dir then
- error("feature not enabled")
+ return execute.view { module = "index", view = "404" }
end
local file_list = extos.listdir(config.download_dir)
@@ -58,4 +58,4 @@
}
}
end)
-end)
\ No newline at end of file
+end)
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/download_file.lua
--- a/app/main/index/download_file.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/download_file.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,5 @@
if not config.download_dir then
- error("feature not enabled")
+ return execute.view { module = "index", view = "404" }
end
local filename = param.get("filename")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/index.lua
--- a/app/main/index/index.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/index.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,114 +1,88 @@
-local function getIssuesSelector()
- return Issue:new_selector()
- :add_order_by([[
- coalesce(
- issue.fully_frozen + issue.voting_time,
- issue.half_frozen + issue.verification_time,
- issue.accepted + issue.discussion_time,
- issue.created + issue.max_admission_time
- ) - now()
- ]])
+if not app.session:has_access("anonymous") then
+ slot.put(" Closed user group, please login.
")
+ return
+end
+
+local unit_id = request.get_param{ name = "unit" }
+local area_id = request.get_param{ name = "area" }
+
+if unit_id == "all" then
+ unit_id = nil
+end
+
+if area_id == "all" then
+ area_id = nil
+end
+
+local unit
+local area
+
+if unit_id then
+ unit = Unit:by_id(unit_id)
+end
+
+if area_id then
+ area = Area:by_id(area_id)
end
---[[
-ui.title( function ()
- ui.link { attr = { class = "home" }, module = "index", view = "index", text = _"Home" }
-end)
---]]
-
-ui.title()
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
-if false then
-slot.select ( "tabs", function ()
-
- ui.tag {
- attr = { onclick = "showTab(0);" },
- content = "units",
- }
- slot.put ( " " )
- ui.tag {
- attr = { onclick = "showTab(1);" },
- content = "timeline"
- }
- slot.put ( " " )
- ui.tag {
- attr = { onclick = "showTab(2);" },
- content = "what"
- }
- slot.put ( " " )
- ui.tag {
- attr = { onclick = "showTab(3);" },
- content = "members"
- }
-
-end )
+ execute.view{ module = "index", view = "_sidebar_motd_public" }
+
+ execute.view{ module = "issue", view = "_list" }
+ end }
-ui.script { script = [[
-
- var tabs = ["tab1", "main", "tab2", "tab3"]
- var currentId;
-
- function showTab(id) {
- var tabId = tabs[id];
- $('.tab').hide();
- $('.main').hide();
- $('.' + tabId).show();
- currentId = id;
- };
-
- showTab(0);
-
- $(function(){
- // Bind the swipeHandler callback function to the swipe event on div.box
- $( "body" ).on( "swiperight", function swipeHandler( event ) {
- newId = currentId - 1;
- if (newId < 0) return;
- showTab(newId);
- } )
- $( "body" ).on( "swipeleft", function swipeHandler( event ) {
- newId = currentId + 1;
- if (newId > tabs.length - 1) return;
- showTab(newId);
- } )
- });
-
-]]}
-end
-
+ ui.cell_sidebar{ content = function()
+ execute.view{ module = "index", view = "_sidebar_motd" }
+ if app.session.member then
+ execute.view{ module = "index", view = "_sidebar_notifications" }
+ end
+ if config.firstlife then
+ ui.container{ attr = { class = "map mdl-special-card mdl-shadow--2dp pos-before-main" }, content = function()
+ ui.tag{ tag = "iframe", attr = { src = config.firstlife.areaviewer_url .. "?" .. config.firstlife.coordinates .. "&domain=" .. request.get_absolute_baseurl(), class = "map" }, content = "" }
+ end }
+ end
+ if config.map then
+ local initiatives = Initiative:new_selector():exec()
+ local geo_objects = {}
+ for i, initiative in ipairs(initiatives) do
+ if initiative.location and initiative.location.coordinates then
+ local geo_object = {
+ lon = initiative.location.coordinates[1],
+ lat = initiative.location.coordinates[2],
+ label = "i" .. initiative.id,
+ description = slot.use_temporary(function()
+ ui.link{ module = "initiative", view = "show", id = initiative.id, text = initiative.display_name }
+ end),
+ type = "initiative"
+ }
+ table.insert(geo_objects, geo_object)
+ end
+ end
+ if ontomap_get_instances then
+ local instances = ontomap_get_instances()
+ for i, instance in ipairs(instances) do
+ table.insert(geo_objects, instance)
+ end
+ end
+ ui.container{ attr = { class = "map mdl-special-card mdl-shadow--2dp pos-before-main" }, content = function()
+ ui.map(geo_objects)
+ end }
+ end
+ if config.logo then
+ config.logo()
+ end
+ if area then
+ execute.view{ module = "area", view = "_sidebar_whatcanido", params = { area = area } }
+ elseif unit then
+ execute.view{ module = "unit", view = "_sidebar_whatcanido", params = { unit = unit } }
+ else
+ execute.view{ module = "index", view = "_sidebar_whatcanido" }
+ end
+
+ execute.view { module = "index", view = "_sidebar_members" }
+
+ end }
+end }
-if app.session.member then
- execute.view{ module = "index", view = "_sidebar_motd_intern" }
-else
- execute.view{ module = "index", view = "_sidebar_motd_public" }
-end
-
-if app.session:has_access("anonymous") then
- -- show the units the member has voting privileges for
- execute.view {
- module = "index", view = "_sidebar_units", params = {
- member = app.session.member
- }
- }
-end
-
--- show the user what can be done
-execute.view { module = "index", view = "_sidebar_whatcanido" }
-
--- show active members
-if app.session:has_access("all_pseudonymous") then
- execute.view{ module = "index", view = "_sidebar_members" }
-end
-
-if app.session:has_access("anonymous") then
-
- if not app.session.member then
--- execute.view {
--- module = "slideshow", view = "_index"
--- }
- end
-
- execute.view {
- module = "issue", view = "_list2", params = { }
- }
-
-end -- if app.session:has_access "anonymous"
\ No newline at end of file
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/login.lua
--- a/app/main/index/login.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/login.lua Sun Jul 15 14:07:29 2018 +0200
@@ -8,90 +8,131 @@
ui.title(_"Login")
app.html_title.title = _"Login"
-execute.view{ module = "index", view = "_sidebar_motd_public" }
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ execute.view{ module = "index", view = "_sidebar_motd_public" }
-ui.section(function()
+ execute.view{ module = "index", view = "_lang_chooser" }
+
+ ui.heading{ level = 1, content = _"Login" }
-ui.sectionHead(function()
- ui.heading{ level = 1, content = _"Login" }
- ui.container { attr = { class = "right" }, content = function()
- for i, lang in ipairs(config.enabled_languages) do
- local langcode
- locale.do_with({ lang = lang }, function()
- langcode = _("[Name of Language]")
- end)
-
- if i > 1 then
- slot.put(" | ")
+ local redirect_params = {}
+ local redirect_params_string = param.get("redirect_params")
+
+ if redirect_params_string then
+ local tmp = json.import(redirect_params_string)
+ if type(tmp) == "table" then
+ for k, v in pairs(tmp) do
+ if type(v) == "string" then
+ redirect_params[k] = v
+ end
+ end
end
-
- ui.link{
- content = function()
- ui.tag{ content = langcode }
- end,
- module = "index",
- action = "set_lang",
- params = { lang = lang },
- routing = {
- default = {
- mode = "redirect",
- module = request.get_module(),
- view = request.get_view(),
- id = request.get_id_string(),
- params = request.get_param_strings()
+ end
+
+ ui.form{
+ module = 'index',
+ action = 'login',
+ routing = {
+ ok = {
+ mode = 'redirect',
+ module = param.get("redirect_module") or "index",
+ view = param.get("redirect_view") or "index",
+ id = param.get("redirect_id"),
+ params = redirect_params
+ },
+ error = {
+ mode = 'redirect',
+ module = "index",
+ view = "login",
+ params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
}
}
- }
- end
- end }
-end)
-ui.form{
- module = 'index',
- action = 'login',
- routing = {
- ok = {
- mode = 'redirect',
- module = param.get("redirect_module") or "index",
- view = param.get("redirect_view") or "index",
- id = param.get("redirect_id"),
- },
- error = {
- mode = 'forward',
- module = 'index',
- view = 'login',
- }
- },
- content = function()
- ui.sectionRow(function()
- ui.field.text{
- attr = { id = "username_field" },
- label = _'Login name',
- name = 'login',
- value = ''
- }
- ui.script{ script = 'document.getElementById("username_field").focus();' }
- ui.field.password{
- label = _'Password',
- name = 'password',
- value = ''
- }
- ui.container { attr = { class = "actions" }, content = function()
+ },
+ content = function()
+ if slot.get_content("error_code") == "invalid_credentials" then
+ ui.container{ attr = { class = "warning" }, content = _"Invalid login name or password!" }
+ end
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-login__username", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-login__username" },
+ label = _'Login name',
+ name = 'login',
+ value = ''
+ }
+ slot.put(" ")
+ ui.field.password{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-login__password", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-login__password" },
+ label = _'Password',
+ name = 'password',
+ value = ''
+ }
+ slot.put("
")
ui.tag{
tag = "input",
attr = {
type = "submit",
- class = "btn btn-default",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
value = _'Login'
- },
- content = ""
+ }
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" },
+ module = "index", view = "index", text = _"Cancel"
}
- slot.put(" ")
- slot.put(" ")
- ui.link{ module = "index", view = "reset_password", text = _"Forgot password?" }
- slot.put(" ")
- ui.link{ module = "index", view = "send_login", text = _"Forgot login name?" }
- end }
- end )
- end
-}
-end )
\ No newline at end of file
+ if not config.disable_registration then
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "index", view = "register", text = _"No account yet?", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ end
+ if config.self_registration then
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "registration", view = "register", text = _"No account yet?", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ end
+ slot.put("
")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "index", view = "reset_password", text = _"Forgot password?", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "index", view = "send_login", text = _"Forgot login name?", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ end
+ }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/index/register.lua
--- a/app/main/index/register.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/index/register.lua Sun Jul 15 14:07:29 2018 +0200
@@ -5,7 +5,7 @@
end
if config.registration_disabled and not ldap_uid then
- error("registration disabled")
+ return execute.view { module = "index", view = "404" }
end
execute.view{ module = "index", view = "_lang_chooser" }
@@ -33,28 +33,21 @@
-ui.section( function()
-
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ ui.heading{ level = 1, content = _"Account registration" }
+--[[
if not code and not ldap_uid then
- ui.title(_"Registration (step 1 of 3: Invite code)")
- ui.sectionHead( function()
- ui.heading { level = 1, content = _"Invite code" }
- end )
+ ui.heading{ level = 1, content = _"Registration (step 1 of 3: Invite code)" }
elseif (not member.notify_email and not notify_email)
or (not member.name and not name)
or (not member.login and not login and not member.authority)
or step == 1 then
- ui.title(_"Registration (step 2 of 3: Personal information)")
- ui.sectionHead( function()
- ui.heading { level = 1, content = _"Check and enter personal data" }
- end )
+ ui.heading { level = 1, content = _"Registration (step 2 of 3: Personal information)" }
else
- ui.title(_"Registration (step 3 of 3: Terms of use and password)")
- ui.sectionHead( function()
- ui.heading { level = 1, content = _"Read and accept the terms and choose a password" }
- end )
+ ui.heading { level = 1, content = _"Registration (step 3 of 3: Terms of use and password)" }
end
-
+--]]
ui.sectionRow( function()
ui.form{
attr = { class = "wide" },
@@ -64,20 +57,34 @@
code = code,
notify_email = notify_email,
name = name,
- login = login
+ login = login,
+ skip = param.get("skip"),
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
},
content = function()
if not code and not ldap_uid then
ui.field.hidden{ name = "step", value = 1 }
- ui.heading { level = 2, content = _"Please enter the invite code you've received" }
+ ui.tag { tag = "p", content = _"Please enter the invite code you've received" }
ui.field.text{
- name = 'code',
- value = param.get("invite")
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Invite code',
+ name = 'code',
+ value = ''
}
- ui.submit{
- text = _'proceed with registration',
- attr = { class = "btn btn-default" }
+ slot.put("
")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _'proceed with registration'
+ }
}
slot.put(" ")
else
@@ -94,125 +101,182 @@
execute.view{ module = "member", view = "_profile", params = { member = member, for_registration = true } }
- if not util.is_profile_field_locked(member, "notify_email") then
- ui.heading { level = 2, content = _'Email address' }
+ slot.put("
")
+
+ if not util.is_profile_field_locked(member, "notify_email") and not member.notify_email then
ui.tag{
tag = "p",
content = _"Please enter your email address. This address will be used for automatic notifications (if you request them) and in case you've lost your password. This address will not be published. After registration you will receive an email with a confirmation link."
}
ui.field.text{
- name = 'notify_email',
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Email address',
+ name = 'notify_email',
value = param.get("notify_email") or member.notify_email
}
end
if not util.is_profile_field_locked(member, "name") then
- ui.heading { level = 2, content = _'Screen name' }
ui.tag{
tag = "p",
content = _"Please choose a name, i.e. your real name or your nick name. This name will be shown to others to identify you."
}
ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Screen name',
name = 'name',
value = param.get("name") or member.name
}
end
if not util.is_profile_field_locked(member, "login") then
- ui.heading { level = 2, content = _'Login name' }
ui.tag{
tag = "p",
content = _"Please choose a login name. This name will not be shown to others and is used only by you to login into the system. The login name is case sensitive."
}
ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Login name',
name = 'login',
value = param.get("login") or member.login
}
end
- ui.submit{
- text = _'proceed with registration',
- attr = { class = "btn btn-default" }
- }
- slot.put(" ")
- ui.link{
- content = _"one step back",
- module = "index",
- view = "register",
- params = {
- invite = code
+ slot.put("
")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _'proceed with registration'
}
}
- else
-
+ if param.get("skip") ~= "1" then
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ content = _"one step back",
+ module = "index",
+ view = "register",
+ params = {
+ invite = code,
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ end
+ else
ui.field.hidden{ name = "step", value = "3" }
- ui.container{
- attr = { class = "wiki use_terms" },
- content = function()
- slot.put(config.use_terms)
- end
- }
+
+ local need_to_accept_terms = false
for i, checkbox in ipairs(config.use_terms_checkboxes) do
- slot.put(" ")
- ui.tag{
- tag = "div",
+ local member_useterms = MemberUseterms:new_selector()
+ :add_where{ "member_id = ?", member.id }
+ :add_where{ "contract_identifier = ?", checkbox.name }
+ :exec()
+ if #member_useterms == 0 then
+ need_to_accept_terms = true
+ end
+ end
+
+ if need_to_accept_terms then
+ ui.container{
+ attr = { class = "wiki use_terms" },
content = function()
+ slot.put(config.use_terms)
+ end
+ }
+
+ for i, checkbox in ipairs(config.use_terms_checkboxes) do
+ local member_useterms = MemberUseterms:new_selector()
+ :add_where{ "member_id = ?", member.id }
+ :add_where{ "contract_identifier = ?", checkbox.name }
+ :exec()
+ if #member_useterms == 0 then
+ slot.put(" ")
ui.tag{
- tag = "input",
- attr = {
- type = "checkbox",
- id = "use_terms_checkbox_" .. checkbox.name,
- name = "use_terms_checkbox_" .. checkbox.name,
- value = "1",
- style = "float: left;",
- checked = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean) and "checked" or nil
- }
- }
- slot.put(" ")
- ui.tag{
- tag = "label",
- attr = { ['for'] = "use_terms_checkbox_" .. checkbox.name },
- content = function() slot.put(checkbox.html) end
+ tag = "div",
+ content = function()
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "checkbox",
+ id = "use_terms_checkbox_" .. checkbox.name,
+ name = "use_terms_checkbox_" .. checkbox.name,
+ value = "1",
+ style = "float: left;",
+ checked = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean) and "checked" or nil
+ }
+ }
+ slot.put(" ")
+ ui.tag{
+ tag = "label",
+ attr = { ['for'] = "use_terms_checkbox_" .. checkbox.name },
+ content = function() slot.put(checkbox.html) end
+ }
+ end
}
end
- }
- end
+ end
- slot.put(" ")
-
+ slot.put(" ")
+ end
+
member.notify_email = notify_email or member.notify_email
member.name = name or member.name
member.login = login or member.login
- ui.heading { level = 2, content = _"Personal information" }
- execute.view{ module = "member", view = "_profile", params = {
- member = member, include_private_data = true
- } }
- ui.field.text{
- readonly = true,
- label = _'Login name',
- name = 'login',
- value = member.login
- }
+-- ui.heading { level = 2, content = _"Personal information" }
+-- execute.view{ module = "member", view = "_profile", params = {
+-- member = member, include_private_data = true
+-- } }
+-- ui.field.text{
+-- readonly = true,
+-- label = _'Login name',
+-- name = 'login',
+-- value = member.login
+-- }
if not (member.authority == "ldap") then
- ui.heading { level = 2, content = _'Password' }
ui.tag{
tag = "p",
content = _"Please choose a password and enter it twice. The password is case sensitive."
}
ui.field.password{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Password',
name = 'password1',
}
+ slot.put(" ")
ui.field.password{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__code", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__code" },
+ label = _'Repeat password',
name = 'password2',
}
end
- ui.submit{
- text = _'activate account',
- attr = { class = "btn btn-default" }
+ slot.put("
")
- ui.link {
- module = "member", view = "show", id = app.session.member_id,
- content = _"Cancel"
- }
- end )
- end )
- end
-}
\ No newline at end of file
+ end }
+ end }
+ end }
+
+ ui.cell_sidebar{ content = function()
+ execute.view {
+ module = "member", view = "_sidebar_whatcanido", params = {
+ member = app.session.member
+ }
+ }
+ end }
+
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/member/show.lua
--- a/app/main/member/show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/member/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -61,129 +61,141 @@
ui.titleMember(member)
-execute.view {
- module = "member", view = "_sidebar_whatcanido", params = {
- member = member
- }
-}
+ui.grid{ content = function()
+ ui.cell_main{ content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ execute.view{
+ module = "member_image",
+ view = "_show",
+ params = {
+ member = member,
+ image_type = "avatar",
+ show_dummy = true,
+ class = "left",
+ force_update = app.session.member_id == member.id
+ }
+ }
+ slot.put(" ")
+ ui.tag{ content = member.name }
+ end }
+ ui.container {
+ attr = { class = "float-right" },
+ content = function()
+ ui.link{
+ content = _"Account history",
+ module = "member", view = "history", id = member.id
+ }
+ end
+ }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
-execute.view {
- module = "member", view = "_sidebar_contacts", params = {
- member = member
- }
-}
+ if member.identification then
+ ui.container{ content = member.identification }
+ end
+ execute.view{
+ module = "member",
+ view = "_profile",
+ params = { member = member }
+ }
-ui.section( function()
- ui.sectionHead( function()
- execute.view{
- module = "member_image",
- view = "_show",
- params = {
- member = member,
- image_type = "avatar",
- show_dummy = true,
- class = "left",
- force_update = app.session.member_id == member.id
- }
- }
- ui.heading{ level = 1, content = member.name }
- slot.put(" ")
- ui.container {
- attr = { class = "right" },
- content = function()
- ui.link{
- content = _"Account history",
- module = "member", view = "history", id = member.id
+ --[[
+ execute.view {
+ module = "member", view = "_timeline",
+ params = { member = member }
}
- end
- }
- if member.identification then
- ui.container{ content = member.identification }
+ --]]
+ end }
+ end }
+
+ if #initiated_initiatives > 0 then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Initiatives created by this member" }
+ end }
+ ui.container{ attr = { class = "initiative_list" }, content = function()
+ execute.view {
+ module = "initiative", view = "_list",
+ params = { initiatives = initiated_initiatives, for_member = member },
+ }
+ end }
+ end }
+ end
+
+ if #supported_initiatives > 0 then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"What this member is currently supporting" }
+ end }
+ ui.container{ attr = { class = "initiative_list" }, content = function()
+ execute.view {
+ module = "initiative", view = "_list",
+ params = { initiatives = supported_initiatives, for_member = member },
+ }
+ end }
+ end }
end
- end )
- ui.sectionRow( function()
- execute.view{
- module = "member",
- view = "_profile",
- params = { member = member }
- }
- end )
-end )
+
+ if #voted_initiatives > 0 then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"How this member voted" }
+ end }
+ ui.container{ attr = { class = "initiative_list" }, content = function()
+ execute.view {
+ module = "initiative", view = "_list",
+ params = { initiatives = voted_initiatives, for_member = member },
+ }
+ end }
+ end }
+ end
+ --[[
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Outgoing delegations" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ execute.view {
+ module = "delegation", view = "_list",
+ params = { delegations_selector = outgoing_delegations_selector, outgoing = true },
+ }
+ end }
+ end }
-
-ui.section( function()
- ui.sectionHead( function()
- ui.heading { level = 2, content = _"Initiatives created by this member" }
- end )
- ui.sectionRow( function()
- for i, initiative in ipairs(initiated_initiatives) do
- execute.view {
- module = "initiative", view = "_list",
- params = { initiative = initiative },
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Incoming delegations" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ execute.view {
+ module = "delegation", view = "_list",
+ params = { delegations_selector = incoming_delegations_selector, incoming = true },
+ }
+ end }
+ end }
+ --]]
+ end }
+
+ ui.cell_sidebar{ content = function()
+ execute.view {
+ module = "member", view = "_sidebar_whatcanido", params = {
member = member
}
- end
- end )
-end )
+ }
-ui.section( function()
- ui.sectionHead( function()
- ui.heading { level = 2, content = _"What this member is currently supporting" }
- end )
- ui.sectionRow( function()
- for i, initiative in ipairs(supported_initiatives) do
- execute.view {
- module = "initiative", view = "_list",
- params = { initiative = initiative },
+ execute.view {
+ module = "member", view = "_sidebar_contacts", params = {
member = member
}
- end
- end )
-end )
-
-ui.section( function()
- ui.sectionHead( function()
- ui.heading { level = 2, content = _"How this member voted" }
- end )
- ui.sectionRow( function()
- for i, initiative in ipairs(voted_initiatives) do
- execute.view {
- module = "initiative", view = "_list",
- params = { initiative = initiative }
- }
- end
- end )
-end )
-
+ }
+ end }
-ui.section( function()
- ui.sectionHead( function()
- ui.heading { level = 2, content = _"Outgoing delegations" }
- end )
- ui.sectionRow( function()
- execute.view {
- module = "delegation", view = "_list",
- params = { delegations_selector = outgoing_delegations_selector, outgoing = true },
- }
- end )
-end )
-
-
-ui.section( function()
-
- ui.sectionHead( function()
- ui.heading { level = 2, content = _"Incoming delegations" }
- end )
- ui.sectionRow( function()
- execute.view {
- module = "delegation", view = "_list",
- params = { delegations_selector = incoming_delegations_selector, incoming = true },
- }
- end )
-
-end )
-
+end }
if app.session.member_id == member.id then
ui.script{ script = [[
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/member_image/_show.lua
--- a/app/main/member_image/_show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/member_image/_show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -13,15 +13,18 @@
class = ""
end
-
if config.fastpath_url_func then
ui.image{
- attr = { title = popup_text, class = "member_image member_image_" .. image_type .. class },
+ attr = { title = popup_text, class = "mdl-chip__contact member_image member_image_" .. image_type .. class },
external = config.fastpath_url_func(member_id, image_type)
}
else
+ local c = "mdl-chip__contact "
+ if image_type == "photo" then
+ c = ""
+ end
ui.image{
- attr = { title = popup_text, class = "member_image member_image_" .. image_type .. class },
+ attr = { title = popup_text, class = c .. "member_image member_image_" .. image_type .. class },
module = "member_image",
view = "show",
extension = "jpg",
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/_action/accept_scope.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/_action/accept_scope.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,76 @@
+local system_application_id = param.get("system_application_id", atom.integer)
+local domain = param.get("domain")
+local response_type = param.get("response_type")
+
+if domain then
+ domain = string.lower(domain)
+end
+local scopes = {}
+for i = 0, math.huge do
+ scopes[i] = param.get("scope" .. i)
+ if not scopes[i] then
+ break
+ end
+end
+
+local redirect_uri = param.get("redirect_uri")
+local redirect_uri_explicit = param.get("redirect_uri_explicit", atom.boolean)
+local state = param.get("state")
+
+local selector
+
+if system_application_id then
+ selector = MemberApplication:get_selector_by_member_id_and_system_application_id(app.session.member_id, system_application_id)
+else
+ selector = MemberApplication:get_selector_by_member_id_and_domain(app.session.member_id, domain)
+end
+selector:for_update()
+
+local member_application = selector:exec()
+
+if not member_application then
+ member_application = MemberApplication:new()
+ member_application.member_id = app.session.member_id
+ member_application.system_application_id = system_application_id
+ member_application.domain = domain
+end
+
+local new_scopes = {}
+
+for i = 0, #scopes do
+ if scopes[i] then
+ for scope in string.gmatch(scopes[i], "[^ ]+") do
+ new_scopes[scope] = true
+ end
+ end
+end
+
+if member_application.scopes then
+ for scope in string.gmatch(member_application.scopes, "[^ ]+") do
+ new_scopes[scope] = true
+ end
+end
+
+local new_scopes_list = {}
+
+for k, v in pairs(new_scopes) do
+ new_scopes_list[#new_scopes_list+1] = k
+end
+
+local new_scopes_string = table.concat(new_scopes_list, " ")
+
+member_application.scope = new_scopes_string
+
+member_application:save()
+
+execute.chunk{ module = "oauth2", chunk = "_authorization", params = {
+ member_id = app.session.member_id,
+ system_application_id = system_application_id,
+ domain = domain,
+ session_id = app.session.id,
+ redirect_uri = redirect_uri,
+ redirect_uri_explicit = redirect_uri_explicit,
+ scopes = scopes,
+ state = state,
+ response_type = response_type
+} }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/_authorization.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/_authorization.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,70 @@
+local member_id = param.get("member_id", atom.integer)
+local system_application_id = param.get("system_application_id", atom.integer)
+local domain = param.get("domain")
+local session_id = param.get("session_id", atom.integer)
+local redirect_uri = param.get("redirect_uri")
+local redirect_uri_explicit = param.get("redirect_uri_explicit", atom.boolean)
+local scopes = param.get("scopes", "table")
+local state = param.get("state")
+local response_type = param.get("response_type")
+
+if response_type == "code" then
+
+ local token = Token:create_authorization(
+ member_id,
+ system_application_id,
+ domain,
+ session_id,
+ redirect_uri,
+ redirect_uri_explicit,
+ scopes,
+ state
+ )
+
+ request.redirect{
+ external = redirect_uri,
+ params = { code = token.token, state = state }
+ }
+
+
+elseif response_type == "token" then
+
+ local expiry = db:query({ "SELECT now() + (? || 'sec')::interval AS access", config.oauth2.access_token_lifetime }, "object").access
+
+ local anchor_params = {
+ state = state,
+ expires_in = config.oauth2.access_token_lifetime,
+ token_type = "bearer"
+ }
+
+ for i = 0, #scopes do
+ if scopes[i] then
+ local access_token = Token:new()
+ access_token.token_type = "access"
+ access_token.member_id = member_id
+ access_token.system_application_id = system_application_id
+ access_token.domain = domain
+ access_token.session_id = session_id
+ access_token.expiry = expiry
+ access_token.scope = scopes[i]
+ access_token:save()
+ local index = i == 0 and "" or i
+ anchor_params["access_token" .. index] = access_token.token
+ end
+ end
+
+ local anchor_params_list = {}
+ for k, v in pairs(anchor_params) do
+ anchor_params_list[#anchor_params_list+1] = k .. "=" .. encode.url_part(v)
+ end
+ local anchor = table.concat(anchor_params_list, "&")
+
+ request.redirect{
+ external = redirect_uri .. "#" .. anchor
+ }
+
+else
+
+ error("Internal error, should not happen")
+
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/_filter_view/10_transaction.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/_filter_view/10_transaction.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,5 @@
+db:query("BEGIN;")
+
+execute.inner()
+
+db:query("COMMIT;")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/authorization.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/authorization.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,267 @@
+local function show_error(text)
+ ui.title("Authorization")
+ ui.section(function()
+ ui.sectionHead(function()
+ ui.heading{ content = _"Error during authorization" }
+ end)
+ ui.sectionRow(function()
+ ui.container{ content = text }
+ end )
+ end )
+end
+
+local client_id = param.get("client_id")
+local redirect_uri = param.get("redirect_uri")
+local redirect_uri_explicit = redirect_uri and true or false
+local response_type = param.get("response_type")
+local state = param.get("state")
+
+local no_scope_requested = true
+
+local scopes = {
+ [0] = param.get("scope")
+}
+
+for i = 1, math.huge do
+ scopes[i] = param.get("scope" .. i)
+ if not scopes[i] then
+ break
+ end
+end
+
+if #scopes == 0 and not scopes[0] then
+ scopes[0] = "identification"
+end
+
+local requested_scopes = {}
+
+for i = 0, #scopes do
+ if scopes[i] then
+ for scope in string.gmatch(scopes[i], "[^ ]+") do
+ requested_scopes[scope] = true
+ end
+ end
+end
+
+local system_application
+local member_application
+local client_name
+local scopes_to_accept = table.new(requested_scopes)
+local accepted_scopes = {}
+
+local domain
+
+if client_id then
+ domain = string.match(client_id, "^dynamic:([a-z0-9.-]+)$")
+end
+
+local dynamic_application_check
+if domain then
+ if #domain > 255 then
+ return show_error(_"Domain too long")
+ end
+ if string.find(domain, "^%.") or string.find(domain, "%.$") or string.find(domain, "%.%.") then
+ return show_error(_"Invalid domain format")
+ end
+ if redirect_uri then
+ local redirect_uri_domain, magic = string.match(redirect_uri, "^[Hh][Tt][Tt][Pp][Ss]://([A-Za-z0-9_.-]+)/(.*)$")
+ if not redirect_uri_domain or string.lower(redirect_uri_domain) ~= domain or magic ~= config.oauth2.endpoint_magic then
+ return show_error(_"Redirect URI forbidden")
+ end
+ else
+ redirect_uri = "https://" .. domain .. "/" .. config.oauth2.endpoint_magic
+ end
+ dynamic_application_check = DynamicApplicationScope:check_scopes(domain, redirect_uri, response_type, requested_scopes)
+ if dynamic_application_check == "not_registered" then
+ return show_error(_"Redirect URI or response type not registered")
+ end
+ client_name = domain
+ member_application = MemberApplication:by_member_id_and_domain(app.session.member_id, domain)
+ if member_application then
+ for scope in string.gmatch(member_application.scope, "[^ ]+") do
+ accepted_scopes[scope] = true
+ scopes_to_accept[scope] = nil
+ end
+ end
+else
+ system_application = SystemApplication:by_client_id(client_id)
+ if system_application then
+ if redirect_uri_explicit then
+ if
+ redirect_uri ~= system_application.default_redirect_uri
+ and not SystemApplicationRedirectUri:by_pk(system_application.id, redirect_uri)
+ then
+ return show_error(_"Redirect URI invalid")
+ end
+ else
+ redirect_uri = system_application.default_redirect_uri
+ end
+ if system_application.flow ~= response_type then
+ return show_error(_"Response type not allowed for given client")
+ end
+ client_name = system_application.name
+ member_application = MemberApplication:by_member_id_and_system_application_id(app.session.member_id, system_application.id)
+ end
+end
+
+if not client_name then
+ return show_error(_"Client ID invalid")
+end
+
+local function error_redirect(error_code, description)
+ local params = {
+ state = state,
+ error = error_code,
+ error_description = description
+ }
+ if response_type == "token" then
+ local anchor_params_list = {}
+ for k, v in pairs(params) do
+ anchor_params_list[#anchor_params_list+1] = k .. "=" .. encode.url_part(v)
+ end
+ local anchor = table.concat(anchor_params_list, "&")
+ request.redirect{
+ external = redirect_uri .. "#" .. anchor
+ }
+ else
+ request.redirect{
+ external = redirect_uri,
+ params = params
+ }
+ end
+end
+
+if response_type ~= "code" and response_type ~= "token" then
+ return error_redirect("unsupported_response_type", "Invalid response type")
+end
+
+for i = 0, #scopes do
+ if scopes[i] == "" then
+ return error_redirect("invalid_scope", "Empty scope requested")
+ end
+end
+
+for scope in pairs(requested_scopes) do
+ local scope_valid = false
+ for i, entry in ipairs(config.oauth2.available_scopes) do
+ if scope == entry.scope or scope == entry.scope .. "_detached" then
+ scope_valid = true
+ break
+ end
+ end
+ if not scope_valid then
+ return error_redirect("invalid_scope", "Requested scope not available")
+ end
+end
+
+if system_application then
+ if system_application.permitted_scope then
+ local permitted_scopes = {}
+ for scope in string.gmatch(system_application.permitted_scope, "[^ ]+") do
+ permitted_scopes[scope] = true
+ end
+ for scope in pairs(requested_scopes) do
+ if not permitted_scopes[scope] then
+ return error_redirect("invalid_scope", "Scope not permitted")
+ end
+ end
+ end
+ if system_application.forbidden_scope then
+ for scope in string.gmatch(system_application.forbidden_scope, "[^ ]+") do
+ if requested_scopes[scope] then
+ return error_redirect("invalid_scope", "Scope forbidden")
+ end
+ end
+ end
+ if system_application.automatic_scope then
+ for scope in string.gmatch(system_application.automatic_scope, "[^ ]+") do
+ scopes_to_accept[scope] = nil
+ accepted_scopes[scope] = true
+ end
+ end
+ if member_application then
+ for scope in string.gmatch(member_application.scope, "[^ ]+") do
+ scopes_to_accept[scope] = nil
+ accepted_scopes[scope] = true
+ end
+ end
+else
+ if dynamic_application_check == "missing_scope" then
+ return error_redirect("invalid_scope", "Scope not permitted")
+ end
+end
+
+if next(scopes_to_accept) then
+ ui.title("Application authorization")
+ ui.section(function()
+ ui.sectionHead(function()
+ ui.heading{ content = client_name }
+ ui.heading{ content = "wants to access your account" }
+ end)
+ if not system_application and not member_application then
+ ui.sectionRow(function()
+ ui.container{ content = _"Warning: Untrusted third party application." }
+ end)
+ end
+ ui.sectionRow(function()
+ ui.heading{ level = 3, content = _"Requested privileges:" }
+ ui.tag{ tag = "ul", attr = { class = "ul" }, content = function()
+ for i, entry in ipairs(config.oauth2.available_scopes) do
+ local name = entry.name[locale.get("lang")] or entry.scope
+ if accepted_scopes[entry.scope] or requested_scopes[entry.scope] or accepted_scopes[entry.scope .. "_detached"] or requested_scopes[entry.scope .. "_detached"] then
+ ui.tag{ tag = "li", content = function()
+ ui.tag{ content = name }
+ if accepted_scopes[entry.scope .. "_detached"] or requested_scopes[entry.scope .. "_detached"] then
+ slot.put(" ")
+ ui.tag{ content = _"(detached)" }
+ end
+ if scopes_to_accept[entry.scope] or scopes_to_accept[entry.scope .. "_detached"] then
+ slot.put(" ")
+ ui.tag{ content = _"(new)" }
+ end
+ -- TODO display changes
+ end }
+ end
+ end
+ end }
+ end )
+ local params = {
+ system_application_id = system_application and system_application.id or nil,
+ domain = domain,
+ redirect_uri = redirect_uri,
+ redirect_uri_explicit = redirect_uri_explicit,
+ state = state,
+ response_type = response_type
+ }
+ for i = 0, #scopes do
+ params["scope" .. i] = scopes[i]
+ end
+ ui.form{
+ module = "oauth2", action = "accept_scope", params = params,
+ routing = { default = { mode = "redirect", module = "oauth2", view = "authorization", params = request.get_param_strings() } },
+ content = function()
+ ui.sectionRow(function()
+ ui.submit{ text = _"Grant authorization", attr = { class = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored " } }
+ slot.put(" ")
+ ui.link{ content = _"Decline authorization", attr = { class = "mdl-button mdl-js-button" }, external = redirect_uri, params = { error = "access_denied", error_description = "User declined to authorize client" } }
+ end )
+ end
+ }
+ end )
+else
+
+ execute.chunk{ module = "oauth2", chunk = "_authorization", params = {
+ member_id = app.session.member_id,
+ system_application_id = system_application and system_application.id or nil,
+ domain = domain,
+ session_id = app.session.id,
+ redirect_uri = redirect_uri,
+ redirect_uri_explicit = redirect_uri_explicit,
+ scopes = scopes,
+ state = state,
+ response_type = response_type
+ } }
+
+
+end
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/register.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/register.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,77 @@
+if not request.is_post() then
+ return execute.view { module = "index", view = "405" }
+end
+
+slot.set_layout(nil, "application/json;charset=UTF-8")
+
+local r = json.object()
+
+local function error_result(error_code, error_description)
+ -- TODO special HTTP status codes for some errors?
+ request.set_status("400 Bad Request")
+ slot.put_into("data", json.export{
+ error = error_code,
+ error_description = error_description
+ })
+end
+
+local client_id = param.get("client_id")
+local flow = param.get("flow")
+local scope = param.get("scope")
+
+if flow ~= "code" and flow ~= "token" then
+ return error_result("invalid_request", "invalid flow")
+end
+
+local domain
+
+if client_id then
+ domain = string.match(client_id, "^dynamic:([a-z0-9.-]+)$")
+ if not domain then
+ return error_result("invalid_client", "invalid client_id (use lower case host name prefixed with 'dynamic:')")
+ end
+end
+
+local cert_ca = request.get_header("X-LiquidFeedback-CA")
+local cert_distinguished_name = request.get_header("X-SSL-DN")
+local cert_common_name
+
+if cert_distinguished_name then
+ cert_common_name = string.match(cert_distinguished_name, "%f[^/\0]CN=([A-Za-z0-9_.-]+)%f[/\0]")
+ if not cert_common_name then
+ return error_result("invalid_client", "CN in X.509 certificate invalid")
+ end
+else
+ return error_result("invalid_client", "X.509 client authorization missing")
+end
+
+if cert_ca ~= "public" then
+ return error_result("invalid_client", "X.509 certificate not signed by publicly trusted certificate authority or wrong endpoint used")
+end
+
+if domain then
+ if domain ~= cert_common_name then
+ return error_result("invalid_grant", "CN in X.509 certificate incorrect")
+ end
+else
+ domain = cert_common_name
+end
+
+local redirect_uri = "https://" .. domain .. "/" .. config.oauth2.endpoint_magic
+
+local expiry = db:query({ "SELECT now() + (? || 'sec')::interval AS expiry", config.oauth2.dynamic_registration_lifetime }, "object").expiry
+
+for s in string.gmatch(scope, "[^ ]+") do
+ local dynamic_application_scope = DynamicApplicationScope:new()
+ dynamic_application_scope.redirect_uri = redirect_uri
+ dynamic_application_scope.flow = flow
+ dynamic_application_scope.scope = s
+ dynamic_application_scope.expiry = expiry
+ dynamic_application_scope:upsert_mode()
+ dynamic_application_scope:save()
+end
+
+r.client_id = "dynamic:" .. domain
+r.expires_in = config.oauth2.dynamic_registration_lifetime
+
+slot.put_into("data", json.export(r))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/session.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/session.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,32 @@
+if not request.is_post() then
+ return execute.view { module = "index", view = "405" }
+end
+
+slot.set_layout(nil, "application/json")
+
+local r = json.object{
+ member_id = json.null
+}
+
+if app.session.member_id then
+ local origin = request.get_header("Origin")
+ if origin then
+ local system_applications = SystemApplication:by_origin(origin)
+ if #system_applications > 0 then
+ r.member_id = app.session.member_id
+ r.real_member_id = app.session.real_member_id
+ if app.session.member.role then
+ r.member_is_role = true
+ end
+ else
+ local member_application = MemberApplication:by_member_id_and_origin(app.session.member_id, origin)
+ if member_application then
+ r.member_id = app.session.member_id
+ r.real_member_id = app.session.real_member_id
+ end
+ end
+ end
+end
+
+slot.put_into("data", json.export(r))
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/token.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/token.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,274 @@
+if not request.is_post() then
+ return execute.view { module = "index", view = "405" }
+end
+
+slot.set_layout(nil, "application/json;charset=UTF-8")
+
+request.add_header("Cache-Control", "no-store")
+request.add_header("Pragma", "no-cache")
+
+local function error_result(error_code, error_description)
+ -- TODO special HTTP status codes for some errors?
+ request.set_status("400 Bad Request")
+ slot.put_into("data", json.export{
+ error = error_code,
+ error_description = error_description
+ })
+end
+
+local token
+local grant_type = param.get("grant_type")
+if grant_type == "authorization_code" then
+ local code = param.get("code")
+ token = Token:by_token_type_and_token("authorization", code)
+elseif grant_type == "refresh_token" then
+ local refresh_token = param.get("refresh_token")
+ token = Token:by_token_type_and_token("refresh", refresh_token)
+elseif grant_type == "access_token" then
+ local access_token, access_token_err = util.get_access_token()
+ if access_token_err then
+ if access_token_err == "header_and_param" then
+ return error_result("invalid_request", "Access token passed both via header and param")
+ end
+ error("Error in util.get_access_token")
+ end
+ token = Token:by_token_type_and_token("access", access_token)
+else
+ return error_result("unsupported_grant_type", "Grant type not supported")
+end
+
+if not token then
+ return error_result("invalid_grant", "Token invalid or expired")
+end
+
+if grant_type == "authorization_code" then
+ if not token.used then
+ local expiry = db:query({"SELECT now() + (? || 'sec')::interval AS expiry", config.oauth2.authorization_code_lifetime }, "object").expiry
+ token.used = true
+ token.expiry = expiry
+ token:save()
+ else
+ token:destroy()
+ return error_result("invalid_grant", "Token invalid or expired")
+ end
+end
+
+if grant_type ~= "access_token" then
+ local cert_ca = request.get_header("X-LiquidFeedback-CA")
+ local cert_distinguished_name = request.get_header("X-SSL-DN")
+ local cert_common_name
+ if cert_distinguished_name then
+ cert_common_name = string.match(cert_distinguished_name, "%f[^/\0]CN=([A-Za-z0-9_.-]+)%f[/\0]")
+ if not cert_common_name then
+ return error_result("invalid_client", "CN in X.509 certificate invalid")
+ end
+ else
+ return error_result("invalid_client", "X.509 client authorization missing")
+ end
+ if token.system_application then
+ if cert_ca ~= "private" then
+ return error_result("invalid_client", "X.509 certificate not signed by private certificate authority or wrong endpoint used")
+ end
+ if cert_common_name ~= token.system_application.cert_common_name then
+ return error_result("invalid_grant", "CN in X.509 certificate incorrect")
+ end
+ else
+ if cert_ca ~= "public" then
+ return error_result("invalid_client", "X.509 certificate not signed by publicly trusted certificate authority or wrong endpoint used")
+ end
+ if cert_common_name ~= token.domain then
+ return error_result("invalid_grant", "CN in X.509 certificate incorrect")
+ end
+ end
+ local client_id = param.get("client_id")
+ if client_id then
+ if token.system_application then
+ if client_id ~= token.system_application.client_id then
+ return error_result("invalid_grant", "Token was issued to another client")
+ end
+ else
+ if client_id ~= "dynamic:" .. token.domain then
+ return error_result ("invalid_grant", "Token was issued to another client")
+ end
+ end
+ elseif grant_type == "authorization_code" and not cert_common_name then
+ return error_result("invalid_request", "No client_id supplied for authorization_code request")
+ end
+end
+
+if grant_type == "authorization_code" then
+ local redirect_uri = param.get("redirect_uri")
+ if (token.redirect_uri_explicit or redirect_uri) and token.redirect_uri ~= redirect_uri then
+ return error_result("invalid_request", "Redirect URI missing or invalid")
+ end
+end
+
+local scopes = {
+ [0] = param.get("scope")
+}
+for i = 1, math.huge do
+ scopes[i] = param.get("scope" .. i)
+ if not scopes[i] then
+ break
+ end
+end
+
+if not scopes[0] and #scopes == 0 then
+ for dummy, token_scope in ipairs(token.token_scopes) do
+ scopes[token_scope.index] = token_scope.scope
+ end
+end
+
+local allowed_scopes = {}
+local requested_detached_scopes = {}
+for scope in string.gmatch(token.scope, "[^ ]+") do
+ allowed_scopes[scope] = true
+end
+for i = 0, #scopes do
+ if scopes[i] then
+ for scope in string.gmatch(scopes[i], "[^ ]+") do
+ if string.match(scope, "_detached$") then
+ requested_detached_scopes[scope] = true
+ end
+ if not allowed_scopes[scope] then
+ return error_result("invalid_scope", "Scope exceeds limits")
+ end
+ end
+ end
+end
+
+local expiry
+
+if grant_type == "access_token" then
+ expiry = db:query({ "SELECT FLOOR(EXTRACT(EPOCH FROM ? - now())) AS access_time_left", token.expiry }, "object")
+else
+ expiry = db:query({
+ "SELECT now() + (? || 'sec')::interval AS refresh, now() + (? || 'sec')::interval AS access",
+ config.oauth2.refresh_token_lifetime,
+ config.oauth2.access_token_lifetime
+ }, "object")
+end
+
+if grant_type == "refresh_token" then
+ local requested_detached_scopes_list = {}
+ for scope in pairs(requested_detached_scopes) do
+ requested_detached_scopes_list[#requested_detached_scopes_list+1] = scope
+ end
+ local tokens_to_reduce = Token:old_refresh_token_by_token(token, requested_detached_scopes_list)
+ for dummy, t in ipairs(tokens_to_reduce) do
+ local t_scopes = {}
+ for t_scope in string.gmatch(t.scope, "[^ ]+") do
+ t_scopes[t_scope] = true
+ end
+ for scope in pairs(requested_detached_scopes) do
+ local scope_without_detached = string.gmatch(scope, "(.+)_detached")
+ if t_scope[scope] then
+ t_scope[scope] = nil
+ t_scope[scope_without_detached] = true
+ end
+ end
+ local t_scope_list = {}
+ for scope in pairs(t_scopes) do
+ t_scope_list[#t_scope_list+1] = scope
+ end
+ t.scope = table.concat(t_scope_list, " ")
+ t:save()
+ end
+end
+
+local r = json.object()
+
+local refresh_token
+if
+ grant_type ~= "access_token"
+ and (grant_type == "authorization_code" or #(Token:fresh_refresh_token_by_token(token)) == 0)
+then
+ refresh_token = Token:new()
+ refresh_token.token_type = "refresh"
+ if grant_type == "authorization_code" then
+ refresh_token.authorization_token_id = token.id
+ else
+ refresh_token.authorization_token_id = token.authorization_token_id
+ end
+ refresh_token.member_id = token.member_id
+ refresh_token.system_application_id = token.system_application_id
+ refresh_token.domain = token.domain
+ refresh_token.session_id = token.session_id
+ refresh_token.expiry = expiry.refresh
+ refresh_token.scope = token.scope
+ refresh_token:save()
+ r.refresh_token = refresh_token.token
+end
+
+
+r.token_type = "bearer"
+if grant_type == "access_token" then
+ r.expires_in = expiry.access_time_left
+else
+ r.expires_in = config.oauth2.access_token_lifetime
+end
+
+for i = 0, #scopes do
+ if scopes[i] then
+ local scope = scopes[i]
+ local access_token = Token:new()
+ access_token.token_type = "access"
+ if grant_type == "authorization_code" then
+ access_token.authorization_token_id = token.id
+ else
+ access_token.authorization_token_id = token.authorization_token_id
+ end
+ access_token.member_id = token.member_id
+ access_token.system_application_id = token.system_application_id
+ access_token.domain = token.domain
+ access_token.session_id = token.session_id
+ if grant_type == "access_token" then
+ access_token.expiry = token.expiry
+ else
+ access_token.expiry = expiry.access
+ end
+ access_token.scope = scope
+ access_token:save()
+ if refresh_token then
+ local refresh_token_scope = TokenScope:new()
+ refresh_token_scope.token_id = refresh_token.id
+ refresh_token_scope.index = i
+ refresh_token_scope.scope = scope
+ refresh_token_scope:save()
+ end
+ local index = i == 0 and "" or i
+ r["access_token" .. index] = access_token.token
+ end
+end
+
+r.member_id = token.member_id
+if token.member.role then
+ r.member_is_role = true
+end
+if token.session then
+ r.real_member_id = token.real_member_id
+end
+
+if param.get("include_member", atom.boolean) then
+ if allowed_scopes.identification or allowed_scopes.authentication then
+ local member = token.member
+ r.member = json.object{
+ id = member.id,
+ name = member.name,
+ }
+ if token.session and token.session.real_member then
+ r.real_member = json.object{
+ id = token.session.real_member.id,
+ name = token.session.real_member.name,
+ }
+ end
+ if allowed_scopes.identification then
+ r.member.identification = member.identification
+ if token.session and token.session.real_member then
+ r.real_member.identification = token.session.real_member.identification
+ end
+ end
+ end
+end
+
+slot.put_into("data", json.export(r))
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/oauth2/validate.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/oauth2/validate.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,85 @@
+if not request.is_post() then
+ return execute.view { module = "index", view = "405" }
+end
+
+slot.set_layout(nil, "application/json")
+
+local function error_result(error_code, description)
+ local r = json.object()
+ r.error = error_code
+ r.error_description = description
+ slot.put_into("data", json.export(r))
+ request.set_status("400 Bad Request")
+end
+
+local access_token, access_token_err = util.get_access_token()
+
+if access_token_err then
+ if access_token_err == "header_and_param" then
+ return error_result("invalid_request", "Access token passed both via header and param")
+ end
+ error("Error in util.get_access_token")
+end
+
+if not access_token then
+ return error_result("invalid_token", "No access token supplied")
+end
+
+local token = Token:by_token_type_and_token("access", access_token)
+
+if not token then
+ return error_result("invalid_token", "Access token invalid")
+end
+
+local scopes = {}
+for scope in string.gmatch(token.scope, "[^ ]+") do
+ local match = string.match(scope, "(.+)_detached$")
+ scopes[match or scope] = true
+end
+local scope_list = {}
+for scope in pairs(scopes) do
+ scope_list[#scope_list+1] = scope
+end
+table.sort(scope_list)
+local scope = table.concat(scope_list, " ")
+
+local r = json.object()
+r.scope = scope
+r.member_id = token.member_id
+if token.member.role then
+ r.member_is_role = true
+end
+if token.session then
+ r.real_member_id = token.session.real_member_id
+end
+
+if param.get("include_member", atom.boolean) then
+ if scopes.identification or scopes.authentication then
+ local member = token.member
+ r.member = json.object{
+ id = member.id,
+ name = member.name,
+ }
+ if token.session and token.session.real_member then
+ r.real_member = json.object{
+ id = token.session.real_member.id,
+ name = token.session.real_member.name,
+ }
+ end
+ if scopes.identification then
+ r.member.identification = member.identification
+ if token.session and token.session.real_member then
+ r.real_member.identification = token.session.real_member.identification
+ end
+ end
+ if param.get("include_member_notify_email", atom.boolean) then
+ r.member.notify_email = member.notify_email
+ end
+ end
+end
+
+r.logged_in = token.session_id and true or false
+slot.put_into("data", json.export(r))
+
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/opinion/_action/update.lua
--- a/app/main/opinion/_action/update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/opinion/_action/update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -41,7 +41,7 @@
end
if degree ~= 0 and not app.session.member:has_voting_right_for_unit_id(suggestion.initiative.issue.area.unit_id) then
- error("access denied")
+ return execute.view { module = "index", view = "403" }
end
if not opinion then
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/policy/_list.lua
--- a/app/main/policy/_list.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/policy/_list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -20,11 +20,12 @@
ui.heading { level = 3, content = policy.name }
- ui.tag{
- content = policy.description
- }
-
- slot.put ( " " )
+ if policy.description and #(policy.description) > 0 then
+ ui.tag{
+ content = policy.description
+ }
+ slot.put ( " " )
+ end
ui.link {
attr = {
@@ -55,8 +56,8 @@
if policy.polling then
ui.field.text{ label = _"New" .. ":", value = _"without" }
else
- ui.field.text{ label = _"New" .. ":", value = "≤ min " .. format.interval_text(policy.max_admission_time) }
- ui.field.text{ label = _"New" .. ":", value = "≤ max " .. format.interval_text(policy.max_admission_time) }
+ ui.field.text{ label = _"New" .. ":", value = "≥ " .. format.interval_text(policy.min_admission_time) }
+ ui.field.text{ label = _"New" .. ":", value = "≤ " .. format.interval_text(policy.max_admission_time) }
end
ui.field.text{ label = _"Discussion" .. ":", value = format.interval_text(policy.discussion_time) or _"variable" }
ui.field.text{ label = _"Frozen" .. ":", value = format.interval_text(policy.verification_time) or _"variable" }
@@ -69,7 +70,7 @@
else
ui.field.text{
label = _"Issue quorum" .. ":",
- value = "≥ " .. tostring(policy.issue_quorum_num) .. "/" .. tostring(policy.issue_quorum_den)
+ value = "≥ " .. tostring(policy.issue_quorum)
}
end
ui.field.text{
@@ -88,4 +89,4 @@
}
end
}
-end
\ No newline at end of file
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/_action/register.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/_action/register.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,296 @@
+local function check_italian_mobile_phone_number(value)
+
+ if not value then
+ return false
+ end
+
+ value = string.gsub(value, "[^0-9]*", "")
+
+ if #(value) < 9 or #(value) > 10 then
+ return false
+ end
+
+ local mobile_phone_prefixes = {
+ { min = 320, max = 329, comment = "Wind Tre" },
+ { min = 330, max = 339, comment = "Telecom Italia (TIM)" },
+ { min = 340, max = 349, comment = "Vodafone Omnitel" },
+ { min = 350, max = 359, comment = "" },
+ { min = 360, max = 369, comment = "Telecom Italia (TIM)" },
+ { min = 370, max = 379, comment = "" },
+ { min = 380, max = 389, comment = "Wind Tre" },
+ { min = 390, max = 393, comment = "Wind Tre" },
+ { min = 394, max = 399, comment = "Wind Tre" }
+ }
+
+ local value_prefix = tonumber(string.match(value, "^(...)"))
+
+ local valid_prefix = false
+
+ for i, prefix in ipairs(mobile_phone_prefixes) do
+ trace.debug(value_prefix, prefix.min)
+ if value_prefix >= prefix.min and value_prefix <= prefix.max then
+ valid_prefix = true
+ end
+ end
+
+ if valid_prefix then
+ return true
+ else
+ return false
+ end
+end
+
+local function check_uk_mobile_phone_number(value)
+
+ if not value then
+ return false
+ end
+
+ value = string.gsub(value, "[^0-9]*", "")
+
+ if #(value) < 11 or #(value) > 11 then
+ return false
+ end
+
+ local mobile_phone_prefixes = {
+ { min = 071, max = 079, comment = "UK phone" },
+ }
+
+ local value_prefix = tonumber(string.match(value, "^(...)"))
+
+ local valid_prefix = false
+
+ for i, prefix in ipairs(mobile_phone_prefixes) do
+ trace.debug(value_prefix, prefix.min)
+ if value_prefix >= prefix.min and value_prefix <= prefix.max then
+ valid_prefix = true
+ end
+ end
+
+ if valid_prefix then
+ return true
+ else
+ return false
+ end
+end
+
+local errors = 0
+
+local manual_verification
+
+if config.self_registration.allow_bypass_checks and param.get("manual_verification") then
+ manual_verification = true
+end
+
+for i, checkbox in ipairs(config.use_terms_checkboxes) do
+ local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
+ if not accepted then
+ slot.put_into("error", checkbox.not_accepted_error)
+ errors = errors + 1
+ end
+end
+
+local email = param.get("email")
+
+local members = Member:new_selector()
+ :add_where{ "notify_email = ? OR notify_email_unconfirmed = ?", email }
+ :exec()
+
+if #members > 0 then
+ slot.select("error", function()
+ slot.put_into("registration_register_email_invalid", "already_used")
+ ui.tag{ content = _"This email address already been used. Please check your inbox for an invitation or contact us." }
+ end)
+ errors = errors + 1
+end
+
+local verification = Verification:new()
+verification.requested = "now"
+verification.request_origin = json.object{
+ ip = request.get_header("X-Forwarded-For"),
+ hostname = request.get_header("X-Forwarded-Host")
+}
+verification.request_data = json.object()
+
+for i, field in ipairs(config.self_registration.fields) do
+ if field.name == "date_of_birth" then
+ local day = tonumber(param.get("verification_data_" .. field.name .. "_day"))
+ local month = tonumber(param.get("verification_data_" .. field.name .. "_month"))
+ local year = tonumber(param.get("verification_data_" .. field.name .. "_year"))
+ local date = atom.date:new{ year = year, month = month, day = day }
+ if date.invalid then
+ slot.select("error", function()
+ ui.container{ content = _"Please check date of birth" }
+ slot.put_into("self_registration__invalid_" .. field.name, "invalid")
+ end)
+ errors = errors + 1
+ end
+ local today = atom.date:get_current()
+ local date_16y_ago = atom.date:new{ year = today.year - 16, month = today.month, day = today.day }
+ if date_16y_ago.invalid and today.month == 2 and today.day == 29 then
+ date_16y_ago = atom.date:new{ year = today.year - 16, month = 2, day = 28 }
+ end
+ if date > date_16y_ago then
+ request.redirect{ external = encode.url { module = "registration", view = "register_rejected_age" } }
+ return
+ end
+ verification.request_data[field.name] = string.format("%04i-%02i-%02i", year, month, day)
+
+ else
+ local value = param.get("verification_data_" .. field.name)
+ if not value or (#value < 1 and (not manual_verification or field.name ~= "mobile_phone")) then
+ slot.put_into("self_registration__invalid_" .. field.name, "to_short")
+ slot.select("error", function()
+ ui.container{ content = _("Please enter: #{field_name}", { field_name = field.label }) }
+ end)
+ errors = errors + 1
+ end
+ if field.name == "fiscal_code" then
+ value = string.upper(value)
+ value = string.gsub(value, "[^A-Z0-9]", "")
+ elseif field.name == "mobile_phone" then
+ value = string.gsub(value, "[^0-9]", "")
+ else
+ value = string.gsub(value, "^%s+", "")
+ value = string.gsub(value, "%s+$", "")
+ value = string.gsub(value, "%s+", " ")
+ end
+ verification.request_data[field.name] = value
+ end
+end
+
+local automatic_verification_possible = true
+
+local mobile_phone = verification.request_data.mobile_phone
+
+if not manual_verification then
+ if config.self_registration.check_for_italien_mobile_phone then
+ if not check_italian_mobile_phone_number(mobile_phone) then
+ slot.select("error", function()
+ ui.container{ content = _"Please check the mobile phone number (invalid format)" }
+ end)
+ errors = errors + 1
+ end
+ end
+
+ if config.self_registration.check_for_uk_mobile_phone then
+ if not check_uk_mobile_phone_number(mobile_phone) then
+ slot.select("error", function()
+ ui.container{ content = _"Please check the mobile phone number (invalid format)" }
+ end)
+ errors = errors + 1
+ end
+ end
+end
+
+if config.self_registration.check_for_italian_fiscal_code then
+ local check_fiscal_code = execute.chunk{ module = "registration", chunk = "_check_fiscal_code" }
+
+ local fiscal_code_valid, fiscal_code_error = check_fiscal_code(
+ verification.request_data.fiscal_code,
+ {
+ first_name = verification.request_data.first_name,
+ last_name = verification.request_data.name,
+ year = tonumber(string.match(verification.request_data.date_of_birth, "^(....)-..-..$")),
+ month = tonumber(string.match(verification.request_data.date_of_birth, "^....-(..)-..$")),
+ day = tonumber(string.match(verification.request_data.date_of_birth, "^....-..-(..)$")),
+ }
+ )
+
+ if fiscal_code_valid then
+ verification.comment = (verification.comment or "").. " /// Fiscal code matched"
+ else
+ slot.select("error", function()
+ ui.container{ content = _"Please check the fiscal code (invalid format or does not match name, first name and/or date of birth)" }
+ end)
+ errors = errors + 1
+ --table.insert(manual_check_reasons, "fiscal code does not match (" .. fiscal_code_error .. ")")
+ end
+end
+
+if errors > 0 then
+ return false
+end
+
+local member = Member:new()
+member.notify_email = email
+member:save()
+
+for i, checkbox in ipairs(config.use_terms_checkboxes) do
+ local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
+ local member_useterms = MemberUseterms:new()
+ member_useterms.member_id = member.id
+ member_useterms.contract_identifier = checkbox.name
+ member_useterms:save()
+end
+
+verification.requesting_member_id = member.id
+
+local manual_check_reasons = {}
+
+if manual_verification then
+ table.insert(manual_check_reasons, "User requested manual verification (during step 1)")
+end
+
+local existing_verifications = Verification:new_selector()
+ :add_where{ "request_data->>'mobile_phone' = ?", mobile_phone }
+ :add_where("comment ilike '%SMS code%'")
+ :exec()
+
+if #existing_verifications > 0 then
+ table.insert(manual_check_reasons, "mobile phone number already used before")
+end
+
+if #manual_check_reasons > 0 then
+ local reasons = table.concat(manual_check_reasons, ", ")
+ verification.comment = (verification.comment or "").. " /// Manual verification needed: " .. reasons
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_manual_check_needed" } }
+
+else
+ local pin = multirand.string(6, "0123456789")
+ verification.request_data.sms_code = pin
+ verification.request_data.sms_code_tries = 3
+ local sms_text = config.self_registration.sms_text
+ local sms_text = string.gsub(sms_text, "{PIN}", pin)
+ print("SMS Code: " .. sms_text)
+ local phone_number
+ if config.self_registration.sms_strip_leading_zero then
+ phone_number = string.match(verification.request_data.mobile_phone, "0(.+)")
+ else
+ phone_number = verification.request_data.mobile_phone
+ end
+ phone_number = config.self_registration.sms_prefix .. phone_number
+ local params = {
+ id = config.self_registration.sms_id,
+ pass = config.self_registration.sms_pass,
+ gateway = config.self_registration.sms_gateway,
+ absender = config.self_registration.sms_from,
+ text = sms_text,
+ nummer = phone_number,
+ test = config.self_registration.test and "1" or nil
+ }
+ local params_list = {}
+ for k, v in pairs(params) do
+ table.insert(params_list, encode.url_part(k) .. "=" .. encode.url_part(v))
+ end
+
+ local params_string = table.concat(params_list, "&")
+ local url = "http://gateway.any-sms.biz/send_sms.php?" .. params_string
+ print("curl " .. url)
+ local output, err, status = extos.pfilter(nil, "curl", url)
+ print(output)
+ verification.request_data.sms_code_sent_status = output
+ if not string.match(output, "^err:0") then
+ verification.comment = (verification.comment or "").. " /// Manual verification needed: sending SMS failed (" .. output .. ")"
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_manual_check_needed" } }
+ return
+ end
+ verification.comment = (verification.comment or "") .. " /// SMS code " .. pin .. " sent"
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_enter_pin", id = verification.id } }
+end
+
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/_action/register_pin.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/_action/register_pin.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,71 @@
+local id = param.get_id()
+local verification = Verification:by_id(id)
+
+if not verification then
+ return false
+end
+
+local pin = param.get("pin")
+
+if param.get("manual_verification") then
+ verification.comment = (verification.comment or "") .. " /// User requested manual verification (during step 2)"
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_manual_check_needed" } }
+ return false
+elseif verification.request_data.sms_code ~= pin then
+ verification.request_data.sms_code_tries = verification.request_data.sms_code_tries - 1
+ verification.comment = (verification.comment or "") .. " /// User entered wrong PIN " .. pin
+ if verification.request_data.sms_code_tries > 0 then
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_enter_pin", id = verification.id, params = { invalid_pin = true } } }
+ return false
+ else
+ verification.comment = (verification.comment or "") .. " /// Manual verification needed: user entered invalid PIN three times"
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_manual_check_needed" } }
+ return false
+ end
+end
+
+verification.comment = (verification.comment or "").. " /// User entered correct PIN code"
+
+verification.verified = "now"
+verification.verification_data = verification.request_data
+
+local identification = config.self_registration.identification_func(verification.request_data)
+
+local members_with_same_identification = Member:new_selector()
+ :add_where{ "identification = ?", identification }
+ :exec()
+
+if #members_with_same_identification > 0 then
+ verification.comment = (verification.comment or "").. " /// Manual verification needed: user with same name already exists"
+ verification:save()
+ request.redirect{ external = encode.url { module = "registration", view = "register_manual_check_needed" } }
+ return false
+end
+
+local member = Member:by_id(verification.requesting_member_id)
+
+member.identification = identification
+member.notify_email = verification.request_data.email
+
+member:send_invitation()
+
+for i, unit_id in ipairs(config.self_registration.grant_privileges_for_unit_ids) do
+ local privilege = Privilege:new()
+ privilege.member_id = member.id
+ privilege.unit_id = unit_id
+ privilege.initiative_right = true
+ privilege.voting_right = true
+ privilege:save()
+end
+
+verification.verified_member_id = member.id
+
+verification.comment = (verification.comment or "").. " /// Account created"
+
+verification:save()
+
+
+request.redirect{ external = encode.url { module = "registration", view = "register_completed" } }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/_action/update_vote.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/_action/update_vote.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,81 @@
+if not app.session.member then
+ return
+end
+
+local cancel = param.get("cancel") and true or false
+if cancel then return true end
+
+local issue = Issue:new_selector():add_where{ "id = ?", param.get("issue_id", atom.integer) }:for_share():single_object_mode():exec()
+
+
+if not app.session.member:has_voting_right_for_unit_id(issue.area.unit_id) then
+ return execute.view { module = "index", view = "403" }
+end
+
+if issue.state ~= "voting" and not issue.closed then
+ slot.put_into("error", _"Voting has not started yet.")
+ return false
+end
+
+if issue.phase_finished or issue.closed and not update_comment then
+ slot.put_into("error", _"This issue is already closed.")
+ return false
+end
+
+local direct_voter = DirectVoter:by_pk(issue.id, app.session.member_id)
+
+if param.get("discard") then
+ if direct_voter then
+ direct_voter:destroy()
+ end
+ slot.put_into("notice", _"Your vote has been discarded. Delegation rules apply if set.")
+ return
+end
+
+local initiatives = issue:get_reference_selector("initiatives")
+ :add_where("initiative.admitted")
+ :add_order_by("initiative.satisfied_supporter_count DESC")
+ :exec()
+
+local vote_for_initiative_id = tonumber(param.get("vote_for_initiative_id"))
+
+local voted = 0
+
+for i, initiative in ipairs(initiatives) do
+ if initiative.id == vote_for_initiative_id then
+ voted = voted + 1
+ end
+end
+
+if voted ~= 1 then
+ slot.put_into("error", _"Please choose one project to vote for.")
+ return false
+end
+
+if not direct_voter then
+ direct_voter = DirectVoter:new()
+ direct_voter.issue_id = issue.id
+ direct_voter.member_id = app.session.member_id
+ direct_voter:save()
+else
+ local votes = Vote:new_selector()
+ :add_where{ "vote.issue_id = ?", issue.id }
+ :add_where{ "vote.member_id = ?", app.session.member_id }
+ :exec()
+ for i, vote in ipairs(votes) do
+ vote:destroy()
+ end
+end
+
+for i, initiative in ipairs(initiatives) do
+ local vote = Vote:new()
+ vote.issue_id = issue.id
+ vote.initiative_id = initiative.id
+ vote.member_id = app.session.member_id
+ if initiative.id == vote_for_initiative_id then
+ vote.grade = 1
+ else
+ vote.grade = 0
+ end
+ vote:save()
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/_check_fiscal_code.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/_check_fiscal_code.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,198 @@
+local oddmap = {
+ [0] = 1, 0, 5, 7, 9, 13, 15, 17, 19, 21,
+ 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16,
+ 10, 22, 25, 24, 23
+}
+
+local monthtable = {
+ "A", "B", "C", "D", "E", "H", "L", "M", "P", "R", "S", "T"
+}
+
+local function removeaccent(str)
+ local gsub = string.gsub
+ str = gsub(str, "\195\129", "A")
+ str = gsub(str, "\195\128", "A")
+ str = gsub(str, "\195\161", "a")
+ str = gsub(str, "\195\160", "a")
+ str = gsub(str, "\195\137", "E")
+ str = gsub(str, "\195\136", "E")
+ str = gsub(str, "\195\169", "e")
+ str = gsub(str, "\195\168", "e")
+ str = gsub(str, "\195\141", "I")
+ str = gsub(str, "\195\140", "I")
+ str = gsub(str, "\195\173", "i")
+ str = gsub(str, "\195\172", "i")
+ str = gsub(str, "\195\147", "O")
+ str = gsub(str, "\195\146", "O")
+ str = gsub(str, "\195\179", "o")
+ str = gsub(str, "\195\178", "o")
+ str = gsub(str, "\195\154", "U")
+ str = gsub(str, "\195\153", "U")
+ str = gsub(str, "\195\186", "u")
+ str = gsub(str, "\195\185", "u")
+ return str
+end
+
+local function normalize_name(str)
+ local gsub = string.gsub
+ str = removeaccent(str)
+ str = gsub(str, " ", "")
+ str = gsub(str, "-", "")
+ str = gsub(str, "'", "")
+ str = gsub(str, "\226\128\146", "")
+ str = gsub(str, "\226\128\147", "")
+ str = gsub(str, "\226\128\148", "")
+ if string.find(str, "^[A-Za-z]+$") then
+ return string.upper(str)
+ else
+ return nil
+ end
+end
+
+local function remove_consonants(str)
+ return (string.gsub(str, "[BCDFGHJKLMNPQRSTVWXYZ]", ""))
+end
+
+local function remove_vowels(str)
+ return (string.gsub(str, "[AEIOU]", ""))
+end
+
+local function numberize(str)
+ local gsub = string.gsub
+ str = gsub(str, "L", "0")
+ str = gsub(str, "M", "1")
+ str = gsub(str, "N", "2")
+ str = gsub(str, "P", "3")
+ str = gsub(str, "Q", "4")
+ str = gsub(str, "R", "5")
+ str = gsub(str, "S", "6")
+ str = gsub(str, "T", "7")
+ str = gsub(str, "U", "8")
+ str = gsub(str, "V", "9")
+ return str
+end
+
+return function(code, data)
+ local sub = string.sub
+ local byte = string.byte
+ local byte0 = byte("0")
+ local byteA = byte("A")
+ local function byteat(str, pos)
+ return (byte(sub(str, pos, pos)))
+ end
+ if #code ~= 16 then
+ return false, "Invalid length"
+ end
+ local sum = 0
+ for i = 1, 15, 2 do
+ local b = byteat(code, i)
+ local b0 = b - byte0
+ if b0 >= 0 and b0 <= 9 then
+ sum = sum + oddmap[b0]
+ else
+ local bA = b - byteA
+ if bA >= 0 and bA <= 25 then
+ sum = sum + oddmap[bA]
+ else
+ return false, "Invalid character"
+ end
+ end
+ end
+ for i = 2, 14, 2 do
+ local b = byteat(code, i)
+ local b0 = b - byte0
+ if b0 >= 0 and b0 <= 9 then
+ sum = sum + b0
+ else
+ local bA = b - byteA
+ if bA >= 0 and bA <= 25 then
+ sum = sum + bA
+ else
+ return false, "Invalid character"
+ end
+ end
+ end
+ local check = byteat(code, 16)
+ local checkA = check - byteA
+ if checkA >= 0 and checkA <= 25 then
+ if checkA ~= sum % 26 then
+ return false, "Invalid checksum"
+ end
+ else
+ local check0 = check - byte0
+ if check0 >= 0 and check0 <= 9 then
+ return false, "Checksum must not be numeric"
+ else
+ return false, "Invalid character"
+ end
+ end
+ if data then
+ if data.last_name then
+ local name = normalize_name(data.last_name)
+ if not name then
+ return false, "Invalid last name"
+ end
+ local consonants = remove_vowels(name)
+ local short = sub(consonants, 1, 3)
+ if #short < 3 then
+ local vowels = remove_consonants(name)
+ short = short .. sub(vowels, 1, 3 - #short)
+ while #short < 3 do
+ short = short .. "X"
+ end
+ end
+ if short ~= sub(code, 1, 3) then
+ return false, "Last name not matching"
+ end
+ end
+ if data.first_name then
+ local name = normalize_name(data.first_name)
+ if not name then
+ return false, "Invalid first name"
+ end
+ local consonants = remove_vowels(name)
+ local short
+ if #consonants >= 4 then
+ short = sub(consonants, 1, 1) .. sub(consonants, 3, 4)
+ else
+ short = consonants
+ if #short < 3 then
+ local vowels = remove_consonants(name)
+ short = short .. sub(vowels, 1, 3 - #short)
+ while #short < 3 do
+ short = short .. "X"
+ end
+ end
+ end
+ if short ~= sub(code, 4, 6) then
+ return false, "First name not matching"
+ end
+ end
+ if data.year then
+ local year = tostring(data.year % 100)
+ if #year < 2 then
+ year = "0" .. year
+ end
+ if year ~= numberize(sub(code, 7, 8)) then
+ return false, "Year of birth not matching"
+ end
+ end
+ if data.month then
+ local monthchar = monthtable[data.month]
+ if monthchar ~= sub(code, 9, 9) then
+ return false, "Month of birth not matching"
+ end
+ end
+ if data.day then
+ local day = tostring(data.day)
+ if #day < 2 then
+ day = "0" .. day
+ end
+ local daycode = numberize(sub(code, 10, 11))
+ if day ~= daycode and tostring(day + 40) ~= daycode then
+ return false, "Day of birth not matching"
+ end
+ end
+ end
+ return true
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/_register_form.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/_register_form.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,102 @@
+for i, field in ipairs(config.self_registration.fields) do
+ local class = ""
+ local field_error = slot.get_content("self_registration__invalid_" .. field.name)
+ if field_error == "" then
+ field_error = nil
+ end
+ if field_error then
+ class = " is-invalid"
+ end
+ if field.name == "date_of_birth" then
+ slot.put(" ")
+ ui.tag{ tag = "label", attr = { style = "vertical-align: bottom; border-bottom: 1px solid rgba(0,0,0, 0.12); color: #777; font-size: 16px;" }, content = field.label .. ":" }
+ slot.put(" ")
+ local days = { { id = 0, name = _"day" } }
+ for i = 1, 31 do
+ table.insert(days, { id = i, name = i })
+ end
+ local months = {
+ { id = 0, name = _"month" },
+ { id = 1, name = "gennaio" },
+ { id = 2, name = "febbraio" },
+ { id = 3, name = "marzo" },
+ { id = 4, name = "aprile" },
+ { id = 5, name = "maggio" },
+ { id = 6, name = "giugno" },
+ { id = 7, name = "luglio" },
+ { id = 8, name = "agosto" },
+ { id = 9, name = "settembre" },
+ { id = 10, name = "ottobre" },
+ { id = 11, name = "novembre" },
+ { id = 12, name = "dicembre" },
+ }
+ if config.self_registration.lang == "en" then
+ months = {
+ { id = 0, name = _"month" },
+ { id = 1, name = "January" },
+ { id = 2, name = "February" },
+ { id = 3, name = "March" },
+ { id = 4, name = "April" },
+ { id = 5, name = "May" },
+ { id = 6, name = "June" },
+ { id = 7, name = "July" },
+ { id = 8, name = "August" },
+ { id = 9, name = "September" },
+ { id = 10, name = "October" },
+ { id = 11, name = "November" },
+ { id = 12, name = "December" },
+ }
+ end
+ local years = { { id = 0, name = _"year" } }
+ for i = 2002, 1900, -1 do
+ table.insert(years, { id = i, name = i })
+ end
+ ui.field.select{
+ container_attr = { style = "display: inline-block; " },
+ attr = { class = class },
+ foreign_records = days,
+ foreign_id = "id",
+ foreign_name = "name",
+ name = "verification_data_" .. field.name .. "_day",
+ value = tonumber(request.get_param{ name = "verification_data_" .. field.name .. "_day" })
+ }
+ slot.put(" ")
+ ui.field.select{
+ container_attr = { style = "display: inline-block; " },
+ attr = { class = class },
+ foreign_records = months,
+ foreign_id = "id",
+ foreign_name = "name",
+ name = "verification_data_" .. field.name .. "_month",
+ value = tonumber(request.get_param{ name = "verification_data_" .. field.name .. "_month" })
+ }
+ slot.put(" ")
+ ui.field.select{
+ container_attr = { style = "display: inline-block; " },
+ attr = { class = class },
+ foreign_records = years,
+ foreign_id = "id",
+ foreign_name = "name",
+ name = "verification_data_" .. field.name .. "_year",
+ value = tonumber(request.get_param{ name = "verification_data_" .. field.name .. "_year" })
+ }
+ slot.put(" ")
+
+ else
+ if field.name == "mobile_phone" then
+ if config.self_registration.lang ~= "en" then
+ ui.tag{ content = "+39" }
+ slot.put(" ")
+ end
+ end
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" .. class },
+ attr = { id = "lf-register__data_" .. field.name, class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data" .. field.name },
+ label = field.label,
+ name = "verification_data_" .. field.name,
+ value = request.get_param{ name = "verification_data_" .. field.name }
+ }
+ end
+ slot.put(" ")
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/register.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/register.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,95 @@
+ui.title(_"Self registration")
+app.html_title.title = _"Self registration"
+
+slot.put("")
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.form{
+ attr = { onsubmit = "document.getElementById('register_button').disabled = true;" },
+ module = "registration", action = "register",
+ routing = {
+ error = { mode = "forward", module = "registration", view = "register" }
+ },
+ content = function()
+
+ ui.container{ content = config.self_registration.info_top }
+
+ execute.view{ module = "registration", view = "_register_form" }
+
+ ui.container{
+ attr = { class = "use_terms" },
+ content = function()
+ slot.put(config.use_terms)
+ end
+ }
+
+ for i, checkbox in ipairs(config.use_terms_checkboxes) do
+ ui.tag{ tag = "label", attr = {
+ class = "mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect",
+ ["for"] = "use_terms_checkbox_" .. checkbox.name
+ },
+ content = function()
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "checkbox",
+ class = "mdl-checkbox__input",
+ id = "use_terms_checkbox_" .. checkbox.name,
+ name = "use_terms_checkbox_" .. checkbox.name,
+ value = "1",
+ style = "float: left;",
+ checked = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean) and "checked" or nil
+ }
+ }
+ ui.tag{
+ attr = { class = "mdl-checkbox__label" },
+ content = function() slot.put(checkbox.html) end
+ }
+ end
+ }
+ slot.put("
")
+ end
+
+ ui.container{ content = function()
+ slot.put(config.self_registration.info_bottom)
+ end }
+
+ slot.put(" ")
+
+ ui.tag{
+ tag = "input",
+ attr = {
+ id = "register_button",
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Proceed with registration"
+ }
+ }
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ name = "manual_verification",
+ id = "manual_verification_button",
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined",
+ value = _"Manual verification (w/o mobile)"
+ }
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "index", view = "login", text = _"Cancel", params = {
+ redirect_module = param.get("redirect_module"),
+ redirect_view = param.get("redirect_view"),
+ redirect_id = param.get("redirect_id"),
+ redirect_params = param.get("redirect_params")
+ }
+ }
+ end
+ }
+
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/register_completed.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/register_completed.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,16 @@
+ui.title(_"Self registration")
+app.html_title.title = _"Self registration"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.heading{ content = _"Self registration completed" }
+ slot.put(" ")
+ ui.container { content = _"We have sent you an invitation email to finish the account setup." }
+ slot.put(" ")
+ ui.container { content = _"Please also check your SPAM folder." }
+ slot.put(" ")
+
+
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/register_enter_pin.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/register_enter_pin.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,66 @@
+local id = param.get_id()
+local verification = Verification:by_id(id)
+local invalid_pin = param.get("invalid_pin", atom.boolean)
+
+ui.title(_"Self registration")
+app.html_title.title = _"Self registration"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.heading{ content = _"PIN page" }
+ slot.put(" ")
+ ui.container { content = _"You should receive a PIN code via SMS shortly. Please enter the PIN." }
+
+ if invalid_pin then
+ slot.put(" ")
+ ui.container { attr = { class = "warning" }, content = _"Invalid PIN, please try again!" }
+ slot.put(" ")
+ end
+
+ ui.form{
+ module = "registration", action = "register_pin", id = verification.id,
+ content = function()
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "pin", class = "mdl-textfield__input", autofocus = "autofocus" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "pin" },
+ label = "PIN code",
+ name = "pin"
+ }
+
+ slot.put(" ")
+
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Proceed with registration"
+ }
+ }
+
+ slot.put("
")
+
+ ui.heading{ content = _"No PIN code received?" }
+ slot.put(" ")
+ ui.container { content = _"If you have not received a PIN code, our team will need to check your registration manually. We will be in touch within two working days. Please accept our apologies for the inconvenience." }
+
+ slot.put(" ")
+
+ ui.tag{
+ tag = "input",
+ attr = {
+ name = "manual_verification",
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised",
+ value = _"Start manual verification"
+ }
+ }
+
+ end
+ }
+
+
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/register_manual_check_needed.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/register_manual_check_needed.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,22 @@
+ui.title(_"Self registration")
+app.html_title.title = _"Self registration"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.heading{ content = _"Manual verification needed" }
+ slot.put(" ")
+ ui.container { content = function()
+ ui.tag{ content = "We are sorry but the automatic verification of personal data has not been successful. We will need to verify your information manually. We apologise for the wait, and thank you for your cooperation. Until your information is verified, you can continue to " }
+ ui.link{ content = _"browse the portal as an unregistered user", module = "index", view = "index" }
+ ui.tag{ content = "." }
+ slot.put("
")
+ ui.tag{ content = "For problems related to registration and use of the platform, please email " }
+ ui.link{ external = "mailto:" .. config.self_registration.contact_email, content = config.self_registration.contact_email }
+ ui.tag{ content = "." }
+ end }
+ slot.put(" ")
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration/register_rejected_age.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration/register_rejected_age.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,18 @@
+ui.title(_"Self registration")
+app.html_title.title = _"Self registration"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.heading{ content = _"Registration rejected" }
+ slot.put(" ")
+ ui.container { content = function()
+ ui.tag { content = _"Sorry, but you need to be at least 16 years old to participate. You can " }
+ ui.link{ content = _"browse the platform as a guest", module = "index", view = "index" }
+ ui.tag{ content = "." }
+ end }
+ slot.put(" ")
+
+
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/_action/update_role_verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/_action/update_role_verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,74 @@
+local verification = RoleVerification:by_id(param.get_id())
+
+local function update_data()
+ verification.verification_data = json.object()
+
+ for i, field in ipairs(config.role_registration.fields) do
+ local value = param.get(field.name)
+ value = string.gsub(value, "^%s+", "")
+ value = string.gsub(value, "%s+$", "")
+ value = string.gsub(value, "%s+", " ")
+ verification.verification_data[field.name] = value
+ end
+end
+
+if verification.verified then
+
+ local member = Member:by_id(verification.verified_member_id)
+
+ if param.get("cancel") then
+ db:query({ "SELECT delete_member(?)", member.id })
+ return
+ end
+
+ member.identification = param.get("identification")
+ member.name = param.get("screen_name")
+ member.notify_email = param.get("email")
+ member:save()
+
+ update_data()
+
+ verification:save()
+
+ if param.get("invite") then
+ member:send_invitation()
+ end
+
+elseif param.get("drop") then
+
+ verification.denied = "now"
+ verification:save()
+ return
+
+elseif param.get("accredit") then
+
+ local member = Member:new()
+ member.role = true
+ member.identification = param.get("identification")
+ member.name = param.get("screen_name")
+ member.notify_email = param.get("email")
+ member:save()
+
+ for i, unit_id in ipairs(config.role_registration.grant_privileges_for_unit_ids) do
+ local privilege = Privilege:new()
+ privilege.member_id = member.id
+ privilege.unit_id = unit_id
+ privilege.initiative_right = false -- TODO
+ privilege.voting_right = true
+ privilege:save()
+ end
+
+ local agent = Agent:new()
+ agent.controlled_id = member.id
+ agent.controller_id = verification.requesting_member_id
+ agent:save()
+
+ update_data()
+
+ verification.verified_member_id = member.id
+ verification.verifying_member_id = app.session.member_id
+ verification.verified = "now"
+
+ verification:save()
+
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/_action/update_verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/_action/update_verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,79 @@
+local verification = Verification:by_id(param.get_id())
+
+local function update_data()
+ verification.verification_data = json.object()
+
+ for i, field in ipairs(config.self_registration.fields) do
+ local value = param.get(field.name)
+ if field.name == "fiscal_code" then
+ value = string.gsub(value, "[^A-Z0-9]", "")
+ elseif field.name == "mobile_phone" then
+ value = string.gsub(value, "[^0-9]", "")
+ else
+ value = string.gsub(value, "^%s+", "")
+ value = string.gsub(value, "%s+$", "")
+ value = string.gsub(value, "%s+", " ")
+ end
+ verification.verification_data[field.name] = value
+ end
+end
+
+if verification.verified_member_id then
+
+ local member = Member:by_id(verification.verified_member_id)
+
+ if param.get("cancel") then
+ db:query({ "SELECT delete_member(?)", member.id })
+ return
+ end
+
+ member.identification = param.get("identification")
+ member.notify_email = param.get("email")
+ member:save()
+
+ update_data()
+
+ verification:save()
+
+ if param.get("invite") then
+ member:send_invitation()
+ end
+
+elseif param.get("drop") then
+
+ verification.denied = "now"
+ verification:save()
+ return
+
+elseif param.get("accredit") then
+
+ local member = Member:by_id(verification.requesting_member_id)
+ member.identification = param.get("identification")
+ member.notify_email = param.get("email")
+ member:save()
+ member:send_invitation()
+
+ for i, unit_id in ipairs(config.self_registration.grant_privileges_for_unit_ids) do
+ local privilege = Privilege:new()
+ privilege.member_id = member.id
+ privilege.unit_id = unit_id
+ privilege.initiative_right = true
+ privilege.voting_right = true
+ privilege:save()
+ end
+
+ update_data()
+
+ verification.verified_member_id = verification.requesting_member_id
+ verification.verifying_member_id = app.session.member_id
+ verification.verified = "now"
+
+ verification:save()
+
+
+else
+
+ update_data()
+ verification:save()
+
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/_filter/90_admin.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/_filter/90_admin.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,9 @@
+if not app.session.member.admin then
+ return execute.view { module = "index", view = "403" }
+end
+
+if config.admin_logger then
+ config.admin_logger(request.get_param_strings())
+end
+
+execute.inner()
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/_role_verification_list.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/_role_verification_list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,38 @@
+local verifications = param.get("verifications", "table")
+
+local columns = {
+ {
+ label = _"Requested at",
+ content = function(record)
+ ui.link{ module = "registration_admin", view = "role_verification", id = record.id, content = function()
+ ui.container{ content = format.date(record.requested) }
+ ui.container{ attr = { class = "light" }, content = format.time(record.requested) }
+ end }
+ end
+ }
+}
+
+for i, field in ipairs(config.role_registration.fields) do
+ table.insert(columns, {
+ label = field.label,
+ content = function(record)
+ ui.tag{ content = record.request_data[field.name] }
+ end
+ })
+end
+
+ui.list{
+ records = verifications,
+ columns = columns
+}
+
+slot.put([[]])
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/_verification_list.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/_verification_list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,76 @@
+local verifications = param.get("verifications", "table")
+
+ui.list{
+ records = verifications,
+ columns = {
+ {
+ label = _"Requested at",
+ content = function(record)
+ ui.link{ module = "registration_admin", view = "verification", id = record.id, content = function()
+ ui.container{ content = format.date(record.requested) }
+ ui.container{ attr = { class = "light" }, content = format.time(record.requested) }
+ end }
+ end
+ },
+ {
+ label = _"Name",
+ content = function(record)
+ ui.container{ content = function()
+ ui.tag{ content = (record.verification_data or record.request_data).name }
+ ui.tag{ content = ", " }
+ ui.tag{ content = (record.verification_data or record.request_data).first_name }
+ end }
+ end
+ },
+ --[[
+ {
+ label = _"City",
+ content = function(record)
+ ui.container{ content = (record.verification_data or record.request_data).zip_code }
+ ui.tag{ content = " " }
+ ui.tag{ content = (record.verification_data or record.request_data).city }
+ end
+ },
+ --]]
+ {
+ label = _"Date/place of birth",
+ content = function(record)
+ ui.container{ content = (record.verification_data or record.request_data).date_of_birth }
+ ui.container{ content = (record.verification_data or record.request_data).place_of_birth }
+ end
+ },
+ {
+ label = _"Fiscal code",
+ content = function(record)
+ ui.tag{ content = (record.verification_data or record.request_data).fiscal_code }
+ end
+ },
+ {
+ label = _"Contact",
+ content = function(record)
+ ui.container{ content = function()
+ ui.tag{ content = (record.verification_data or record.request_data).email }
+ end }
+ ui.container{ content = function()
+ ui.tag{ content = config.self_registration.sms_prefix }
+ local phone_number = (record.verification_data or record.request_data).mobile_phone
+ if config.self_registration.sms_strip_leading_zero then
+ phone_number = string.match(phone_number, "0(.+)")
+ end
+ ui.tag{ content = phone_number }
+ end }
+ end
+ }
+ }
+}
+
+slot.put([[]])
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/index.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/index.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,212 @@
+ui.title(_"Usermanagement")
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Usermanagement" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ ui.container{ content = _"User accounts" }
+
+ ui.tag{ tag = "ul", content = function()
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", content = _("Open requests (#{count})", { count = count }) }
+ end }
+
+ ui.tag{ tag = "ul", content = function()
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%User requested manual verification (during step 1)'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "manual_requested", step = 1 }, content = _("Manual verification requested during step 1 (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%User requested manual verification (during step 2)'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "manual_requested", step = 2 }, content = _("Manual verification requested during step 2 (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '% sent'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "pin_sent" }, content = _("PIN code not entered (yet) (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment similar to '%fiscal code does not match[^/]*'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "fiscal_code" }, content = _("Fiscal code does not match (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%mobile phone number already used before'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "mobile_phone" }, content = _("Phone number used before (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%user with same name already exist'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "identification" }, content = _("Identification used before (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%user entered invalid PIN three times'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "invalid_pin" }, content = _("Invalid PIN entered (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("comment ilike '%user with same name already exists'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "same_name_already_exists" }, content = _("User with same name already exists (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_where("not comment ilike '%User requested manual verification'")
+ :add_where("not comment ilike '% sent'")
+ :add_where("not comment similar to '%fiscal code does not match[^/]*'")
+ :add_where("not comment ilike '%mobile phone number already used before'")
+ :add_where("not comment ilike '%user with same name already exist'")
+ :add_where("not comment ilike '%user entered invalid PIN three times'")
+ :add_where("not comment ilike '%user with same name already exists'")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_requests", params = { mode = "other" }, content = _("other reasons (#{count})", { count = count }) }
+ end }
+ end }
+
+ local count = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_accredited", content = _("Accredited (#{count})", { count = count }) }
+ ui.tag{ tag = "ul", content = function()
+
+ local count = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where("member.activated ISNULL")
+ :add_where("member.deleted ISNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_accredited", params = { mode = "not_activated" }, content = _("Account not activated (yet) (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where("member.activated NOTNULL")
+ :add_where("member.deleted ISNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_accredited", params = { mode = "activated" }, content = _("Activated accounts (#{count})", { count = count }) }
+ end }
+
+ local count = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where("member.deleted NOTNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_cancelled", content = _("Cancelled accounts (#{count})", { count = count }) }
+ end }
+ end }
+ end }
+
+ local count = Verification:new_selector()
+ :add_where("denied NOTNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "verification_rejected", content = _("Rejected requests (#{count})", { count = count }) }
+ end }
+
+ end }
+
+ ui.container{ content = _"Role accounts" }
+
+ ui.tag{ tag = "ul", content = function()
+
+ local count = RoleVerification:new_selector()
+ :add_where("verified ISNULL")
+ :add_where("denied ISNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "role_verification_requests", content = _("Open requests (#{count})", { count = count }) }
+ end }
+
+ local count = RoleVerification:new_selector()
+ :add_where("verified NOTNULL")
+ :add_where("denied ISNULL")
+ :join("member", nil, "member.id = role_verification.verified_member_id")
+ :add_where("member.deleted ISNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "role_verification_accredited", content = _("Accredited (#{count})", { count = count }) }
+ end }
+
+ local count = RoleVerification:new_selector()
+ :add_where("verified NOTNULL")
+ :add_where("denied ISNULL")
+ :join("member", nil, "member.id = role_verification.verified_member_id")
+ :add_where("member.deleted NOTNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "role_verification_cancelled", content = _("Cancelled (#{count})", { count = count }) }
+ end }
+
+ local count = RoleVerification:new_selector()
+ :add_where("verified ISNULL")
+ :add_where("denied NOTNULL")
+ :count()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "registration_admin", view = "role_verification_rejected", content = _("Rejected (#{count})", { count = count }) }
+ end }
+
+
+ end }
+
+ end }
+ end }
+
+
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/role_verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/role_verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,168 @@
+local verification = RoleVerification:by_id(param.get_id())
+local data = verification.verification_data or verification.request_data
+
+local identification = config.role_registration.identification_func(data)
+local member
+if verification.verified_member_id then
+ member = Member:by_id(verification.verified_member_id)
+ identification = member.identification
+end
+
+local group, title, view
+if verification.verified then
+ if member.deleted then
+ group = _"Cancelled accounts"
+ title = _"Cancelled account"
+ view = "role_verification_cancelled"
+ else
+ group = _"Accredited users"
+ title = member.identification
+ view = "role_verification_accredited"
+ end
+elseif verification.denied then
+ group = "Rejected requests"
+ title = _"Rejected request"
+ view = "role_verification_rejected"
+else
+ group = "Open requests"
+ title = _"Open request"
+ view = "role_verification_requests"
+end
+
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Rolemanagement" }
+ slot.put ( " » " )
+ ui.link { module = "registration_admin", view = view, content = group }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = title }
+end)
+app.html_title.title = _"Rolemanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = title }
+ end }
+
+
+ ui.form{
+ module = "registration_admin", action = "update_role_verification", id = verification.id,
+ routing = { ok = { mode = "redirect", module = "registration_admin", view = view } },
+ record = data,
+ content = function()
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ for i, field in ipairs(config.role_registration.fields) do
+ ui.container{ content = function()
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__data_" .. field.name, class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data" .. field.name },
+ label = field.label,
+ name = field.name
+ }
+
+ ui.tag{ content = verification.request_data[field.name] }
+ end }
+ end
+
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label", style = "width: 30em;" },
+ attr = { id = "lf-register__data_identification", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data_identification" },
+ label = "Identification",
+ name = "identification",
+ value = identification
+ }
+
+ end }
+
+ local member = Member:by_id(verification.requesting_member_id)
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+ ui.tag{ content = _"Requested by:" }
+ slot.put(" ")
+ ui.link{ content = member.name, module = "member", view = "show", id = member.id }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__actions mdl-card--border" }, content = function()
+
+ if verification.denied then
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ elseif verification.verified then
+ ui.submit{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Save role data" }
+ slot.put(" ")
+ if not member.activated then
+ ui.submit{ name = "invite", attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Send email invitation again" }
+ slot.put(" ")
+ end
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ slot.put(" ")
+ ui.submit{ name = "cancel", attr = { class = "mdl-button mdl-js-button" }, value = _"Delete account" }
+ else
+ ui.submit{ name = "accredit", attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Accredit role" }
+ slot.put(" ")
+ ui.submit{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Save role data" }
+ slot.put(" ")
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ slot.put(" ")
+ ui.submit{ name = "drop", attr = { class = "mdl-button mdl-js-button" }, value = "Reject request" }
+ end
+ end }
+
+ end
+ }
+ end }
+
+ local verifications = RoleVerification:new_selector()
+ :join("member", nil, "member.id = role_verification.verified_member_id")
+ :add_where{ "member.identification = ?", identification }
+ :add_where{ "role_verification.id <> ?", verification.id }
+ :exec()
+
+ if #verifications > 0 then
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Same identification" }
+ end }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ execute.view{ module = "registration_admin", view = "_role_verification_list", params = { verifications = verifications } }
+ end }
+ end }
+ end
+
+ for i, field_name in ipairs(config.role_registration.match_fields) do
+ local field
+ for j, f in ipairs(config.role_registration.fields) do
+ if f.name == field_name then
+ field = f
+ end
+ end
+ local verifications = Verification:new_selector()
+ :add_where("verified NOTNULL")
+ :add_where{ "lower(request_data->>'" .. field.name .. "') = lower(?)", data[field.name] }
+ :add_where{ "verification.id <> ?", verification.id }
+ :exec()
+
+ if #verifications > 0 then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Same " .. field.label }
+ end }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end }
+ end }
+ end
+ end
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/role_verification_accredited.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/role_verification_accredited.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,86 @@
+local mode = param.get("mode")
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Rolemanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Accredited role accounts"}
+end)
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Accredited role accounts" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications_selector = RoleVerification:new_selector()
+ :join("member", nil, "member.id = role_verification.verified_member_id")
+ :add_where("member.deleted ISNULL")
+ :add_order_by("member.identification")
+
+ if mode == "activated" then
+ verifications_selector:add_where("member.activated NOTNULL")
+ elseif mode == "not_activated" then
+ verifications_selector:add_where("member.activated ISNULL")
+ end
+
+ local verifications = verifications_selector:exec()
+
+ if #verifications > 0 then
+ ui.list{
+ records = verifications,
+ columns = {
+ {
+ label = _"Identification",
+ content = function(record)
+ ui.container{ content = function()
+ local member = Member:by_id(record.verified_member_id)
+ if member then
+ ui.link{ module = "registration_admin", view = "role_verification", id = record.id, content = member.identification }
+ end
+ end }
+ end
+ },
+ {
+ label = _"Account",
+ content = function(record)
+ local member = Member:by_id(record.verified_member_id)
+ if member and member.activated then
+ ui.link{ module = "member", view = "show", id = record.verified_member_id, content = "ID " .. record.verified_member_id }
+ else
+ ui.tag{ content = "ID " }
+ ui.tag{ content = record.verified_member_id }
+ ui.tag{ content = ", " }
+ ui.tag{ content = _"not activated (yet)" }
+
+ end
+ end
+ },
+ {
+ label = _"Verified at",
+ content = function(record)
+ ui.tag{ content = format.timestamp(record.verified) }
+ end
+ },
+ {
+ label = _"Verified by",
+ content = function(record)
+ local member = Member:by_id(record.verifying_member_id)
+ ui.link{ module = "member", view = "show", id = member.id, content = member.identification or (member.id .. " " .. member.name) }
+ end
+ },
+ }
+ }
+ end
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/role_verification_cancelled.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/role_verification_cancelled.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,35 @@
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Rolemanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Cancelled accounts"}
+end)
+app.html_title.title = _"Rolemanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Cancelled accounts" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications = RoleVerification:new_selector()
+ :join("member", nil, "member.id = role_verification.verified_member_id")
+ :add_where("member.deleted NOTNULL")
+ :add_order_by("requested DESC")
+ :exec()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_role_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/role_verification_rejected.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/role_verification_rejected.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,35 @@
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Rolemanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Rejected requests"}
+end)
+app.html_title.title = _"Rolemanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Rejected role accreditation requests" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications = RoleVerification:new_selector()
+ :add_where("denied NOTNULL")
+ :add_order_by("requested DESC")
+ :exec()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_role_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/role_verification_requests.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/role_verification_requests.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,42 @@
+local mode = param.get("mode")
+
+local verifications_selector = RoleVerification:new_selector()
+ :add_where("verified ISNULL")
+ :add_where("denied ISNULL")
+ :add_order_by("requested DESC")
+
+local title = _"Open role requests"
+
+local verifications = verifications_selector:exec()
+
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Rolemanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = _"Open requests" }
+end)
+
+app.html_title.title = _"Rolemanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = title }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_role_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,167 @@
+local verification = Verification:by_id(param.get_id())
+local data = verification.verification_data or verification.request_data
+
+local identification = config.self_registration.identification_func(data)
+local member
+if verification.verified_member_id then
+ member = Member:by_id(verification.verified_member_id)
+ identification = member.identification
+end
+
+local group, title, view
+if verification.verified_member_id then
+ if member.deleted then
+ group = _"Cancelled accounts"
+ title = _"Cancelled account"
+ view = "verification_cancelled"
+ else
+ group = _"Accredited users"
+ title = member.identification
+ view = "verification_accredited"
+ end
+elseif verification.denied then
+ group = "Rejected requests"
+ title = _"Rejected request"
+ view = "verification_rejected"
+else
+ group = "Open requests"
+ title = _"Open request"
+ view = "verification_requests"
+end
+
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Usermanagement" }
+ slot.put ( " » " )
+ ui.link { module = "registration_admin", view = view, content = group }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = title }
+end)
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = title }
+ end }
+
+
+ ui.form{
+ module = "registration_admin", action = "update_verification", id = verification.id,
+ routing = { ok = { mode = "redirect", module = "registration_admin", view = view } },
+ record = data,
+ content = function()
+
+ if not verification.verified_member_id then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.container{ content = verification.comment and verification.comment:gsub("///", " / ") or ""}
+ end }
+ end
+
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+
+ for i, field in ipairs(config.self_registration.fields) do
+ ui.container{ content = function()
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" },
+ attr = { id = "lf-register__data_" .. field.name, class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data" .. field.name },
+ label = field.label,
+ name = field.name
+ }
+
+ ui.tag{ content = verification.request_data[field.name] }
+ end }
+ end
+
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label", style = "width: 30em;" },
+ attr = { id = "lf-register__data_identification", class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data_identification" },
+ label = "Identification",
+ name = "identification",
+ value = identification
+ }
+
+ end }
+
+ ui.container{ attr = { class = "mdl-card__actions mdl-card--border" }, content = function()
+
+ if verification.denied then
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ elseif verification.verified_member_id then
+ ui.submit{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Save personal data" }
+ slot.put(" ")
+ if not member.activated then
+ ui.submit{ name = "invite", attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Send email invitation again" }
+ slot.put(" ")
+ end
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ slot.put(" ")
+ ui.submit{ name = "cancel", attr = { class = "mdl-button mdl-js-button" }, value = _"Delete account" }
+ else
+ ui.submit{ name = "accredit", attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Accredit user" }
+ slot.put(" ")
+ ui.submit{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, value = "Save personal data" }
+ slot.put(" ")
+ ui.link{ attr = { class = "mdl-button mdl-js-button mdl-button--raised" }, module = "registration_admin", view = view, content = "Back" }
+ slot.put(" ")
+ ui.submit{ name = "drop", attr = { class = "mdl-button mdl-js-button" }, value = "Reject request" }
+ end
+ end }
+
+ end
+ }
+ end }
+
+ local verifications = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where{ "member.identification = ?", identification }
+ :add_where{ "verification.id <> ?", verification.id }
+ :exec()
+
+ if #verifications > 0 then
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Same identification" }
+ end }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end }
+ end }
+ end
+
+ for i, field_name in ipairs(config.self_registration.match_fields) do
+ local field
+ for j, f in ipairs(config.self_registration.fields) do
+ if f.name == field_name then
+ field = f
+ end
+ end
+ local verifications = Verification:new_selector()
+ :add_where("verified_member_id NOTNULL")
+ :add_where{ "lower(request_data->>'" .. field.name .. "') = lower(?)", data[field.name] }
+ :add_where{ "verification.id <> ?", verification.id }
+ :exec()
+
+ if #verifications > 0 then
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Same " .. field.label }
+ end }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end }
+ end }
+ end
+ end
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/verification_accredited.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/verification_accredited.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,110 @@
+local mode = param.get("mode")
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Usermanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Accredited users"}
+end)
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Accredited users" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications_selector = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where("member.deleted ISNULL")
+
+ if mode == "activated" then
+ verifications_selector:add_where("member.activated NOTNULL")
+ end
+
+ if mode ~= "not_activated" then
+ verifications_selector:add_order_by("member.identification")
+ local verifications = verifications_selector:exec()
+
+ if #verifications > 0 then
+ ui.list{
+ records = verifications,
+ columns = {
+ {
+ label = _"Name",
+ content = function(record)
+ ui.container{ content = function()
+ local member = Member:by_id(record.verified_member_id)
+ if member then
+ ui.link{ module = "registration_admin", view = "verification", id = record.id, content = member.identification }
+ end
+ end }
+ end
+ },
+ --[[
+ {
+ label = _"City",
+ content = function(record)
+ ui.container{ content = (record.verification_data or record.request_data).zip_code }
+ ui.tag{ content = " " }
+ ui.tag{ content = (record.verification_data or record.request_data).city }
+ end
+ },
+ --]]
+ {
+ label = _"Account",
+ content = function(record)
+ local member = Member:by_id(record.verified_member_id)
+ if member and member.activated then
+ ui.link{ module = "member", view = "show", id = record.verified_member_id, content = "ID " .. record.verified_member_id }
+ else
+ ui.tag{ content = "ID " }
+ ui.tag{ content = record.verified_member_id }
+ ui.tag{ content = ", " }
+ ui.tag{ content = _"not activated (yet)" }
+
+ end
+ end
+ },
+ {
+ label = _"Verified at",
+ content = function(record)
+ ui.tag{ content = format.timestamp(record.verified) }
+ end
+ },
+ {
+ label = _"Verified by",
+ content = function(record)
+ if record.verifying_member_id then
+ local member = Member:by_id(record.verifying_member_id)
+ ui.link{ module = "member", view = "show", id = member.id, content = member.identification or (member.id .. " " .. member.name) }
+ local state = string.match(record.comment, "[^/]+$")
+ slot.put(" ")
+ ui.tag{ content = state }
+ elseif record.verified then
+ ui.tag{ content = _"SMS" }
+ end
+
+ end
+ },
+ }
+ }
+ end
+ elseif mode == "not_activated" then
+ verifications_selector:add_where("member.activated ISNULL")
+ :add_order_by("verification.id DESC")
+ local verifications = verifications_selector:exec()
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/verification_cancelled.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/verification_cancelled.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,35 @@
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Usermanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Cancelled accounts"}
+end)
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Cancelled accounts" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications = Verification:new_selector()
+ :join("member", nil, "member.id = verification.verified_member_id")
+ :add_where("member.deleted NOTNULL")
+ :add_order_by("requested DESC")
+ :exec()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/verification_rejected.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/verification_rejected.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,35 @@
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Usermanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = "Rejected requests"}
+end)
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = _"Rejected accreditation requests" }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ local verifications = Verification:new_selector()
+ :add_where("denied NOTNULL")
+ :add_order_by("request_data->>'name', request_data->>'first_name', requested DESC")
+ :exec()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/registration_admin/verification_requests.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/registration_admin/verification_requests.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,78 @@
+local mode = param.get("mode")
+local step = param.get("step", atom.integer)
+
+local verifications_selector = Verification:new_selector()
+ :add_where("verified_member_id ISNULL")
+ :add_where("denied ISNULL")
+ :add_order_by("requested DESC")
+
+local title = _"Open requests"
+
+if mode == "manual_requested" and step == 1 then
+ title = _"Manual verification requested during step 1"
+ verifications_selector:add_where("comment ilike '%User requested manual verification (during step 1)'")
+elseif mode == "manual_requested" and step == 2 then
+ title = _"Manual verification requested during step 2"
+ verifications_selector:add_where("comment ilike '%User requested manual verification (during step 2)'")
+elseif mode == "pin_sent" then
+ title = _"PIN code not entered"
+ verifications_selector:add_where("comment ilike '% sent'")
+elseif mode == "fiscal_code" then
+ title = _"Fiscal code does not match"
+ verifications_selector:add_where("comment similar to '%fiscal code does not match[^/]*'")
+elseif mode == "mobile_phone" then
+ title = _"Phone number used before"
+ verifications_selector:add_where("comment ilike '%mobile phone number already used before'")
+elseif mode == "identification" then
+ title = _"Identification used before"
+ verifications_selector:add_where("comment ilike '%user with same name already exist'")
+elseif mode == "invalid_pin" then
+ title = _"Invalid PIN entered"
+ verifications_selector:add_where("comment ilike '%user entered invalid PIN three times'")
+elseif mode == "same_name_already_exists" then
+ title = _"Same name already exists"
+ verifications_selector:add_where("comment ilike '%user with same name already exists'")
+elseif mode == "other" then
+ title = _"Other reasons"
+ verifications_selector:add_where("not comment ilike '%User requested manual verification'")
+ verifications_selector:add_where("not comment ilike '% sent'")
+ verifications_selector:add_where("not comment similar to '%fiscal code does not match[^/]*'")
+ verifications_selector:add_where("not comment ilike '%mobile phone number already used before'")
+ verifications_selector:add_where("not comment ilike '%user with same name already exist'")
+ verifications_selector:add_where("not comment ilike '%user entered invalid PIN three times'")
+ verifications_selector:add_where("not comment ilike '%user with same name already exists'")
+end
+
+local verifications = verifications_selector:exec()
+
+
+ui.title(function()
+ ui.link { module = "registration_admin", view = "index", content = _"Usermanagement" }
+ slot.put ( " » " )
+ ui.tag { tag = "span", content = _"Open requests" }
+end)
+
+app.html_title.title = _"Usermanagement"
+
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ ui.tag{ content = title }
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+ if #verifications > 0 then
+ execute.view{ module = "registration_admin", view = "_verification_list", params = { verifications = verifications } }
+ end
+
+ end }
+ end }
+
+ end }
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/role/_action/request.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/role/_action/request.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,57 @@
+if not app.session.member or app.session.member.role then
+ return
+end
+
+local errors = 0
+
+if config.use_terms_checkboxes_role then
+ for i, checkbox in ipairs(config.use_terms_checkboxes_role) do
+ local accepted = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean)
+ if not accepted then
+ slot.put_into("error", checkbox.not_accepted_error)
+ errors = errors + 1
+ end
+ end
+end
+
+local email = param.get("email")
+
+local members = Member:new_selector()
+ :add_where{ "notify_email = ? OR notify_email_unconfirmed = ?", email }
+ :exec()
+
+if #members > 0 then
+ slot.select("error", function()
+ slot.put_into("registration_register_email_invalid", "already_used")
+ ui.tag{ content = _"This email address already been used. Please check your inbox for an invitation or contact us." }
+ end)
+ errors = errors + 1
+end
+
+local verification = RoleVerification:new()
+verification.requesting_member_id = app.session.member_id
+verification.requested = "now"
+verification.request_origin = json.object{
+ ip = request.get_header("X-Forwarded-For"),
+ hostname = request.get_header("X-Forwarded-Host")
+}
+verification.request_data = json.object()
+
+for i, field in ipairs(config.role_registration.fields) do
+ local value = param.get("verification_data_" .. field.name)
+ if not value or #value < 1 then
+ slot.put_into("self_registration__invalid_" .. field.name, "to_short")
+ slot.select("error", function()
+ ui.container{ content = _("Please enter: #{field_name}", { field_name = field.label }) }
+ end)
+ errors = errors + 1
+ end
+ value = string.gsub(value, "^%s+", "")
+ value = string.gsub(value, "%s+$", "")
+ value = string.gsub(value, "%s+", " ")
+ verification.request_data[field.name] = value
+end
+
+verification:save()
+
+request.redirect{ external = encode.url { module = "member", view = "show", id = app.session.member_id } }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/role/_action/switch.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/role/_action/switch.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,55 @@
+local id = param.get_id()
+
+local member_id = app.session.real_member_id or app.session.member_id
+
+if id then
+ local member = Member:by_id(id)
+
+ if member.locked then
+ return
+ end
+
+ local agent = Agent:by_pk(member.id, member_id)
+
+ if not agent then
+ return
+ end
+
+ local session = Session:new()
+ session.member_id = member.id
+ session.real_member_id = member_id
+ session:save()
+
+ if not member.activated then
+ member.activated = "now"
+ end
+
+ member.last_login = "now"
+ member.last_activity = "now"
+ member.active = true
+ member:save()
+
+ app.session:destroy()
+
+ request.set_cookie{
+ name = config.cookie_name or "liquid_feedback_session",
+ value = session.ident
+ }
+elseif app.session.real_member_id then
+ local session = Session:new()
+ session.member_id = app.session.real_member_id
+ session:save()
+
+ app.session:destroy()
+
+ request.set_cookie{
+ name = config.cookie_name or "liquid_feedback_session",
+ value = session.ident
+ }
+end
+
+if config.meta_navigation_home_url then
+ request.redirect{ external = config.meta_navigation_home_url }
+else
+ request.redirect{ module = "index", view = "index" }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/role/_request_form.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/role/_request_form.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,19 @@
+for i, field in ipairs(config.role_registration.fields) do
+ local class = ""
+ local field_error = slot.get_content("role_registration__invalid_" .. field.name)
+ if field_error == "" then
+ field_error = nil
+ end
+ if field_error then
+ class = " is-invalid"
+ end
+ ui.field.text{
+ container_attr = { class = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label" .. class },
+ attr = { id = "lf-register__data_" .. field.name, class = "mdl-textfield__input" },
+ label_attr = { class = "mdl-textfield__label", ["for"] = "lf-register__data" .. field.name },
+ label = field.label,
+ name = "verification_data_" .. field.name,
+ value = request.get_param{ name = "verification_data_" .. field.name }
+ }
+ slot.put(" ")
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/role/request.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/role/request.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,96 @@
+ui.titleMember(_"Request role account")
+
+ui.grid{ content = function()
+
+ ui.cell_main{ content = function()
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"Request role accounts" }
+ end }
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+
+
+ ui.form{
+ attr = { onsubmit = "document.getElementById('register_button').disabled = true;" },
+ module = "role", action = "request",
+ routing = {
+ error = { mode = "forward", module = "role", view = "request" }
+ },
+ content = function()
+
+ ui.container{ content = config.role_registration.info_top }
+
+ execute.view{ module = "role", view = "_request_form" }
+
+ ui.container{
+ attr = { class = "use_terms" },
+ content = function()
+ slot.put(config.use_terms_role)
+ end
+ }
+
+ if config.use_terms_checkboxes_role then
+ for i, checkbox in ipairs(config.use_terms_checkboxes_role) do
+ ui.tag{ tag = "label", attr = {
+ class = "mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect",
+ ["for"] = "use_terms_checkbox_" .. checkbox.name
+ },
+ content = function()
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "checkbox",
+ class = "mdl-checkbox__input",
+ id = "use_terms_checkbox_" .. checkbox.name,
+ name = "use_terms_checkbox_" .. checkbox.name,
+ value = "1",
+ style = "float: left;",
+ checked = param.get("use_terms_checkbox_" .. checkbox.name, atom.boolean) and "checked" or nil
+ }
+ }
+ ui.tag{
+ attr = { class = "mdl-checkbox__label" },
+ content = function() slot.put(checkbox.html) end
+ }
+ end
+ }
+ slot.put("
")
+ end
+ end
+
+ ui.container{ content = function()
+ slot.put(config.role_registration.info_bottom)
+ end }
+
+ slot.put(" ")
+
+ ui.tag{
+ tag = "input",
+ attr = {
+ id = "register_button",
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Proceed with registration"
+ }
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--underlined" },
+ module = "member", view = "show", id = app.session.member_id, text = _"Cancel",
+ }
+
+ end }
+ end }
+ end }
+ end }
+
+ ui.cell_sidebar{ content = function()
+ execute.view {
+ module = "member", view = "_sidebar_whatcanido", params = {
+ member = app.session.member
+ }
+ }
+ end }
+
+end }
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/style/_style.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/style/_style.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,91 @@
+local style = param.get("style", "table")
+
+local md_colors = {
+ ["500"] = {
+ red = "244,67,54",
+ pink = "233,30,99",
+ purple = "156,39,176",
+ ["deep-purple"] = "103,58,183",
+ indigo = "63,81,181",
+ blue = "33,150,243",
+ ["light-blue"] = "3,169,244",
+ cyan = "0,188,212",
+ teal = "0,150,136",
+ green = "76,175,80",
+ ["light-green"] = "139,195,74",
+ lime = "205,220,57",
+ yellow = "255,235,59",
+ amber = "255,193,7",
+ orange = "255,152,0",
+ ["deep-orange"] = "255,87,34",
+ brown = "121,85,72",
+ grey = "158,158,158",
+ ["blue-grey"] = "96,125,139",
+ },
+ ["A200"] = {
+ red = "255,82,82",
+ pink = "255,64,129",
+ purple = "224,64,251",
+ ["deep-purple"] = "124,77,255",
+ indigo = "83,109,254",
+ blue = "68,138,255",
+ ["light-blue"] = "64,196,255",
+ cyan = "24,255,255",
+ teal = "100,255,218",
+ green = "105,240,174",
+ ["light-green"] = "178,255,89",
+ lime = "238,255,65",
+ yellow = "255,255,0",
+ amber = "255,215,64",
+ orange = "255,171,64",
+ ["deep-orange"] = "255,110,64",
+ brown ="62,39,35",
+ grey = "33,33,33",
+ ["blue-grey"] = "38,50,56"
+ }
+}
+
+local r = {}
+
+if style.color then
+ r.color = {
+ primary = style.color.primary,
+ primary_dark = style.color.primary_dark,
+ accent = style.color.accent,
+ primary_contrast = style.color.primary_contrast,
+ accent_contrast = style.color.accent_contrast
+ }
+ r.color_rgb = {
+ primary = style.color.primary,
+ accent = style.color.accent
+ }
+elseif style.color_md then
+ r.color_md = {
+ primary = style.color_md.primary,
+ primary_contrast = style.color_md.primary_contrast,
+ accent = style.color_md.accent,
+ accent_contrast = style.color_md.accent_contrast
+ }
+else
+ r.color_md = {
+ primary = "grey",
+ primary_contrast = "dark",
+ accent = "red",
+ accent_contrast = "dark"
+ }
+end
+if not r.color then
+ r.color = {
+ primary = "$palette-" .. r.color_md.primary .. "-500",
+ primary_dark = "$palette-" .. r.color_md.primary .. "-700",
+ accent = "$palette-" .. r.color_md.accent .. "-A200",
+ primary_contrast = "$color-" .. r.color_md.primary_contrast.. "-contrast",
+ accent_contrast = "$color-" .. r.color_md.accent_contrast .. "-contrast"
+ }
+ r.color_rgb = {
+ primary = md_colors["500"][r.color_md.primary],
+ accent = md_colors["A200"][r.color_md.accent]
+ }
+end
+
+return r
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/style/style.css.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/style/style.css.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,47 @@
+slot.set_layout(nil, "text/css")
+
+local style = execute.chunk{ module = "style", chunk = "_style", params = { style = config.style } }
+
+local scss = [[
+@import "../style/mdl/color-definitions";
+$color-primary: ]] .. style.color.primary .. [[;
+$color-primary-dark: ]] .. style.color.primary .. [[;
+$color-primary-contrast: ]] .. style.color.primary_contrast .. [[;
+$color-accent: ]] .. style.color.accent .. [[;
+$color-accent-contrast: ]] .. style.color.accent_contrast .. [[;
+$checkbox-image-path: "]] .. request.get_absolute_baseurl() .. "static/mdl" .. [[";
+@import "../style/mdl/material-design-lite"
+]]
+
+local key = extos.crypt(json.export(style.color), "$1$12345678") -- TODO hash function
+local filename_scss = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "style-" .. key .. ".scss")
+local filename_css = encode.file_path(WEBMCP_BASE_PATH, 'tmp', "style-" .. key .. ".css")
+
+local css_file = io.open(filename_css, "r")
+
+if not config.css then
+ config.css = {}
+end
+
+if not config.css[key] then
+ if css_file then
+ config.css[key] = css_file:read("*a")
+ else
+ local scss_file = assert(io.open(filename_scss, "w"))
+ scss_file:write(scss)
+ scss_file:write("\n")
+ scss_file:close()
+
+ local output, err, status = extos.pfilter(nil, "sassc", filename_scss)
+ if status ~= 0 then
+ error(err)
+ end
+ config.css[key] = output
+ local css_file = assert(io.open(filename_css, "w"))
+ css_file:write(config.css[key])
+ css_file:close()
+ end
+end
+
+slot.put_into("data", config.css[key])
+
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/suggestion/_action/add.lua
--- a/app/main/suggestion/_action/add.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/suggestion/_action/add.lua Sun Jul 15 14:07:29 2018 +0200
@@ -6,7 +6,7 @@
local initiative = Initiative:by_id(param.get("initiative_id", atom.integer))
if not app.session.member:has_voting_right_for_unit_id(initiative.issue.area.unit_id) then
- error("access denied")
+ return execute.view { module = "index", view = "403" }
end
@@ -27,7 +27,8 @@
end
end
if not formatting_engine_valid then
- error("invalid formatting engine!")
+ slot.put_into("error", "invalid formatting engine!")
+ return false
end
if param.get("preview") then
@@ -68,4 +69,4 @@
opinion:save()
-slot.put_into("notice", _"Your suggestion has been added")
\ No newline at end of file
+slot.put_into("notice", _"Your suggestion has been added")
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/unit/_head.lua
--- a/app/main/unit/_head.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/unit/_head.lua Sun Jul 15 14:07:29 2018 +0200
@@ -29,4 +29,4 @@
end
end }
-end )
\ No newline at end of file
+end )
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/unit/_sidebar.lua
--- a/app/main/unit/_sidebar.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/unit/_sidebar.lua Sun Jul 15 14:07:29 2018 +0200
@@ -11,15 +11,6 @@
:add_where{ "area.active" }
:add_order_by("area.name")
-if member then
- areas_selector:left_join (
- "membership", nil,
- { "membership.area_id = area.id AND membership.member_id = ?", member.id }
- )
- areas_selector:add_field("membership.member_id NOTNULL", "subscribed", { "grouped" })
-end
-
-
local areas = areas_selector:exec()
if member then
unit:load_delegation_info_once_for_member_id(member.id)
@@ -77,4 +68,4 @@
end -- if #areas > 0
-end ) -- ui.sidebar
\ No newline at end of file
+end ) -- ui.sidebar
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/unit/_sidebar_whatcanido.lua
--- a/app/main/unit/_sidebar_whatcanido.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/unit/_sidebar_whatcanido.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,69 +1,108 @@
local unit = param.get ( "unit", "table" )
+unit:load_delegation_info_once_for_member_id(app.session.member_id)
-ui.sidebar ( "tab-whatcanido", function ()
+ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = _"What can I do here?" }
+ end }
+ ui.container{ attr = { class = "what-can-i-do-here" }, content = function()
- ui.sidebarHeadWhatCanIDo()
-
- if app.session.member then
-
- if app.session.member:has_voting_right_for_unit_id ( unit.id ) then
- ui.sidebarSection( function ()
-
- if not unit.delegation_info.first_trustee_id then
- ui.heading{ level = 3, content = _"I want to delegate this organizational unit" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ if app.session.member and app.session.member:has_voting_right_for_unit_id ( unit.id ) then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to stay informed" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.tag{ content = _"check your " }
+ ui.link{
+ module = "member", view = "settings_notification",
+ params = { return_to = "home" },
+ text = _"notifications settings"
+ }
+ end }
+ if not config.voting_only then
ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- unit_id = unit.id,
- },
- content = _("choose delegatee", {
- unit_name = unit.name
- })
+ ui.tag{ content = _"subscribe subject areas or add your interested to issues and you will be notified about changes (follow the instruction on the area or issue page)" }
+ end }
+ end
+ end }
+ end }
+
+ if not config.disable_delegations then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ if not unit.delegation_info.first_trustee_id then
+ ui.tag{ content = _"I want to delegate this organizational unit" }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ unit_id = unit.id,
+ },
+ content = _("choose delegatee", {
+ unit_name = unit.name
+ })
+ }
+ end }
+ end }
+ else
+ ui.container { attr = { class = "right" }, content = function()
+ local member = Member:by_id(unit.delegation_info.first_trustee_id)
+ execute.view{
+ module = "member_image",
+ view = "_show",
+ params = {
+ member = member,
+ image_type = "avatar",
+ show_dummy = true
+ }
}
end }
- end }
- else
- ui.container { attr = { class = "right" }, content = function()
- local member = Member:by_id(unit.delegation_info.first_trustee_id)
- execute.view{
- module = "member_image",
- view = "_show",
- params = {
- member = member,
- image_type = "avatar",
- show_dummy = true
- }
- }
- end }
- ui.heading{ level = 3, content = _"You delegated this unit" }
+ ui.tag{ content = _"You delegated this unit" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = function ()
- ui.link {
- module = "delegation", view = "show", params = {
- unit_id = unit.id,
- },
- content = _("change/revoke delegation", {
- unit_name = unit.name
- })
- }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = function ()
+ ui.link {
+ module = "delegation", view = "show", params = {
+ unit_id = unit.id,
+ },
+ content = _("change/revoke delegation", {
+ unit_name = unit.name
+ })
+ }
+ end }
end }
- end }
- end
- end )
+ end
+ end }
+ end
+
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{
+ content = _"I want to vote"
+ }
+ ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"check the issues on the right, and click on 'Vote now' to vote on an issue which is in voting phase." }
+ end }
+ end }
- ui.sidebarSection( function()
- ui.heading { level = 3, content = _"I want to start a new initiative" }
- ui.tag { tag = "ul", attr = { class = "ul" }, content = function ()
- ui.tag { tag = "li", content = _"Open the appropriate subject area where your issue fits in and follow the instruction on that page." }
- end }
- end )
+ else
+ ui.container { attr = { class = "mdl-card__content mdl-card--border" }, content = function ()
+ ui.tag{ content = _"You are not entitled to vote in this unit" }
+ ui.tag{ tag = "ul", content = function()
+ ui.tag{ tag = "li", content = function()
+ ui.link{ module = "index", view = "login", content = _"Login" }
+ end }
+ end }
+ end }
+ end
- else
- ui.sidebarSection( _"You are not entitled to vote in this unit" )
+ if not config.voting_only and app.session.member_id and app.session.member:has_initiative_right_for_unit_id ( unit.id ) then
+ ui.container{ attr = { class = "mdl-card__content mdl-card--border" }, content = function()
+ ui.tag{ content = _"I want to start a new initiative" }
+ ui.tag{ tag = "ul", attr = { class = "ul" }, content = function ()
+ ui.tag { tag = "li", content = _"open the appropriate subject area for your issue and follow the instruction on that page." }
+ end }
+ end }
end
-
- end
+
+ end }
-end )
\ No newline at end of file
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/unit/show.lua
--- a/app/main/unit/show.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/unit/show.lua Sun Jul 15 14:07:29 2018 +0200
@@ -8,6 +8,7 @@
return
end
+app.current_unit = unit
unit:load_delegation_info_once_for_member_id(app.session.member_id)
@@ -38,36 +39,40 @@
:add_where("issue.closed NOTNULL")
:add_order_by("issue.closed DESC")
-
-
-execute.view { module = "unit", view = "_head", params = { unit = unit } }
-
+ ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
+
+ ui.heading{ content = unit.name }
-execute.view {
- module = "unit", view = "_sidebar", params = {
- unit = unit
- }
-}
+ execute.view { module = "unit", view = "_head", params = { unit = unit } }
+
+ execute.view {
+ module = "unit", view = "_sidebar", params = {
+ unit = unit
+ }
+ }
-execute.view {
- module = "unit", view = "_sidebar_whatcanido", params = {
- unit = unit
- }
-}
+ execute.view {
+ module = "unit", view = "_sidebar_whatcanido", params = {
+ unit = unit
+ }
+ }
-execute.view {
- module = "unit", view = "_sidebar_members", params = {
- unit = unit
- }
-}
+ execute.view {
+ module = "unit", view = "_sidebar_members", params = {
+ unit = unit
+ }
+ }
-execute.view {
- module = "issue",
- view = "_list2",
- params = { for_unit = unit, head = function ()
- ui.heading { attr = { class = "left" }, level = 1, content = unit.name }
+ execute.view {
+ module = "issue",
+ view = "_list",
+ params = { for_unit = unit, head = function ()
+ ui.heading { attr = { class = "left" }, level = 1, content = unit.name }
+ end }
+ }
+ end }
end }
-}
--[[
if app.session:has_access("all_pseudonymous") then
@@ -90,4 +95,4 @@
ui.tabs(tabs)
---]]
\ No newline at end of file
+--]]
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/util/_unit_area_filter.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/app/main/util/_unit_area_filter.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,44 @@
+local member = app.session.member
+local units
+if member then
+ units = member:get_reference_selector("units"):add_order_by("name"):exec()
+ units:load_delegation_info_once_for_member_id(member.id)
+else
+ units = Unit:new_selector():add_where("active"):add_order_by("name"):exec()
+end
+
+ui.tag{ tag = "button", attr = { id = "unit-menu", class = "mdl-button mdl-js-button" }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "filter_list" }
+ ui.tag{ content = "All units" }
+end }
+ui.tag{ tag = "ul", attr = { class = "mdl-menu mdl-menu--top-left mdl-js-menu mdl-js-ripple-effect", ["data-mdl-for"]="unit-menu" }, content = function()
+ if #units > 0 then
+ for i, unit in ipairs(units) do
+ local class = "mdl-navigation__link mdl-navigation__head"
+ if i == #units then
+ class = class .. " mdl-menu__item--full-bleed-divider"
+ end
+ ui.tag{ tag = "li", attr = { class = "mdl-menu__item" }, content = function()
+ ui.link{ attr = { class = classx }, content = unit.name, module = "unit", view = "show", id = unit.id }
+ end }
+ end
+ end
+end }
+
+if app.current_unit then
+ ui.tag{ tag = "button", attr = { id = "area-menu", class = "mdl-button mdl-js-button mdl-button--icon" }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "more_vert" }
+ end }
+ ui.tag{ content = "All units" }
+ ui.tag{ tag = "ul", attr = { class = "mdl-menu mdl-menu--top-left mdl-js-menu mdl-js-ripple-effect", ["data-mdl-for"]="area-menu" }, content = function()
+ for i, area in ipairs({}) do
+ local class = "mdl-navigation__link mdl-menu__item--small"
+ if i == #areas then
+ class = class .. " mdl-menu__item--full-bleed-divider"
+ end
+ ui.tag{ tag = "li", attr = { class = "mdl-menu__item" }, content = function()
+ ui.link{ attr = { class = classx }, module = "area", view = "show", id = area.id, content = area.name }
+ end }
+ end
+ end }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/vote/_action/update.lua
--- a/app/main/vote/_action/update.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/vote/_action/update.lua Sun Jul 15 14:07:29 2018 +0200
@@ -3,10 +3,16 @@
local issue = Issue:new_selector():add_where{ "id = ?", param.get("issue_id", atom.integer) }:for_share():single_object_mode():exec()
+-- TODO patch for project voting
+if config.alternative_voting and config.alternative_voting[tostring(issue.policy.id)] then
+ return false
+end
+
+
local preview = (param.get("preview") or param.get("edit")) and true or false
if not app.session.member:has_voting_right_for_unit_id(issue.area.unit_id) then
- error("access denied")
+ return execute.view { module = "index", view = "403" }
end
local update_comment = param.get("update_comment") and true or false
@@ -82,7 +88,8 @@
end
end
if not formatting_engine_valid then
- error("invalid formatting engine!")
+ slot.put_into("error", "invalid formatting engine!")
+ return false
end
end
@@ -116,7 +123,7 @@
local grade = tonumber(grade)
local initiative = Initiative:by_id(initiative_id)
if initiative.issue.id ~= issue.id then
- error("initiative from wrong issue")
+ return execute.view { module = "index", view = "403" }
end
if not preview and not issue.closed then
local vote = Vote:by_pk(initiative_id, app.session.member.id)
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/vote/list.lua
--- a/app/main/vote/list.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/vote/list.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,23 @@
local issue = Issue:by_id(param.get("issue_id"), atom.integer)
+-- TODO patch for project voting
+if not issue.closed and config.alternative_voting and config.alternative_voting[tostring(issue.policy.id)] then
+ local voting_config = config.alternative_voting[tostring(issue.policy.id)]
+
+ local url = encode.url {
+ module = voting_config.module,
+ view = voting_config.view,
+ params = { issue_id = issue.id }
+ }
+
+ return request.redirect{ external = url }
+end
+
+if not issue then
+ execute.view { module = "index", view = "404" }
+ return
+end
+
local member_id = param.get("member_id", atom.integer)
local member
local readonly = false
@@ -7,8 +25,9 @@
local preview = param.get("preview") and true or false
if member_id then
- if not issue.closed then
- error("access denied")
+ if not issue.closed then
+ execute.view{ module = "index", view = "403" }
+ return
end
member = Member:by_id(member_id)
readonly = true
@@ -149,7 +168,7 @@
ui.sidebar( "tab-members", function()
ui.sidebarHead(function()
- ui.heading{ level = 2, content = _"Incoming delegations" }
+ ui.heading{ level = 4, content = _"Incoming delegations" }
end)
execute.view{
module = "member",
@@ -166,407 +185,410 @@
end)
end
-
-ui.section( function()
-
- ui.sectionHead( function()
- if preview then
- ui.heading { level = 1, content = _"Preview of voting ballot" }
- elseif readonly then
- local str = _("Ballot of '#{member_name}'",
- {member_name = string.format('%s',
- encode.url{
- module = "member",
- view = "show",
- id = member.id,
- },
- encode.html(member.name))
- }
- )
- ui.heading { level = 1, content = function () slot.put ( str ) end }
- else
- ui.heading { level = 1, content = _"Voting" }
- end
- end )
-
- ui.sectionRow( function()
+ui.container{ attr = { class = "mdl-grid" }, content = function()
+ ui.container{ attr = { class = "mdl-cell mdl-cell--12-col" }, content = function()
- ui.form{
- record = direct_voter,
- attr = {
- id = "voting_form",
- class = readonly and "voting_form_readonly" or "voting_form_active"
- },
- module = "vote",
- action = "update",
- params = { issue_id = issue.id },
- content = function()
- if not readonly or preview then
- local scoring = param.get("scoring")
- if not scoring then
- for i, initiative in ipairs(initiatives) do
- local vote = initiative.vote
- if vote then
- tempvotings[initiative.id] = vote.grade
- else
- tempvotings[initiative.id] = 0
- end
- end
- local tempvotings_list = {}
- for key, val in pairs(tempvotings) do
- tempvotings_list[#tempvotings_list+1] = tostring(key) .. ":" .. tostring(val)
- end
- if #tempvotings_list > 0 then
- scoring = table.concat(tempvotings_list, ";")
- else
- scoring = ""
- end
+ ui.container{ attr = { class = "mdl-card mdl-card__fullwidth mdl-shadow--2dp" }, content = function()
+ ui.container{ attr = { class = "mdl-card__title mdl-card--border" }, content = function()
+ ui.heading { attr = { class = "mdl-card__title-text" }, level = 2, content = function()
+ if preview then
+ ui.tag{ content = _"Preview of voting ballot" }
+ elseif readonly then
+ local str = _("Ballot of '#{member_name}'", { member_name = string.format(
+ '%s',
+ encode.url{ module = "member", view = "show", id = member.id },
+ encode.html(member.name)
+ ) })
+ ui.tag{ content = function () slot.put ( str ) end }
+ else
+ ui.tag{ content = _"Voting" }
end
- slot.put('')
- end
- if preview then
- ui.heading{ level = 2, content = _"Your choice" }
- elseif not readonly then
- ui.heading{ level = 2, content = _"Make your choice by placing the initiatives" }
- end
-
- ui.container{
- attr = { id = "voting" },
+ end }
+ end }
+
+ ui.container{ attr = { class = "mdl-card__content" }, content = function()
+
+ ui.form{
+ record = direct_voter,
+ attr = {
+ id = "voting_form",
+ class = readonly and "voting_form_readonly" or "voting_form_active"
+ },
+ module = "vote",
+ action = "update",
+ params = { issue_id = issue.id },
content = function()
- local approval_index, disapproval_index = 0, 0
- local approval_used, disapproval_used
- for grade = max_grade, min_grade, -1 do
- local entries = sections[grade]
- local class
- if grade > 0 then
- class = "approval"
- elseif grade < 0 then
- class = "disapproval"
- else
- class = "abstention"
+ if not readonly or preview then
+ local scoring = param.get("scoring")
+ if not scoring then
+ for i, initiative in ipairs(initiatives) do
+ local vote = initiative.vote
+ if vote then
+ tempvotings[initiative.id] = vote.grade
+ else
+ tempvotings[initiative.id] = 0
+ end
+ end
+ local tempvotings_list = {}
+ for key, val in pairs(tempvotings) do
+ tempvotings_list[#tempvotings_list+1] = tostring(key) .. ":" .. tostring(val)
+ end
+ if #tempvotings_list > 0 then
+ scoring = table.concat(tempvotings_list, ";")
+ else
+ scoring = ""
+ end
end
- if
- #entries > 0 or
- (grade == 1 and not approval_used) or
- (grade == -1 and not disapproval_used) or
- grade == 0
- then
- ui.container{
- attr = { class = class },
- content = function()
- local heading
- if class == "approval" then
- approval_used = true
- approval_index = approval_index + 1
- if approval_count > 1 then
- if approval_index == 1 then
- if #entries == 1 then
- heading = _"Approval (first preference) [single entry]"
+ slot.put('')
+ end
+ if preview then
+ ui.container{ content = _"Your choice" }
+ elseif not readonly then
+ ui.container{ content = _"Make your choice by placing the initiatives" }
+ end
+
+ ui.container{
+ attr = { id = "voting" },
+ content = function()
+ local approval_index, disapproval_index = 0, 0
+ local approval_used, disapproval_used
+ for grade = max_grade, min_grade, -1 do
+ local entries = sections[grade]
+ local class
+ if grade > 0 then
+ class = "approval"
+ elseif grade < 0 then
+ class = "disapproval"
+ else
+ class = "abstention"
+ end
+ if
+ #entries > 0 or
+ (grade == 1 and not approval_used) or
+ (grade == -1 and not disapproval_used) or
+ grade == 0
+ then
+ ui.container{
+ attr = { class = class },
+ content = function()
+ local heading
+ if class == "approval" then
+ approval_used = true
+ approval_index = approval_index + 1
+ if approval_count > 1 then
+ if approval_index == 1 then
+ if #entries == 1 then
+ heading = _"Approval (first preference) [single entry]"
+ else
+ heading = _"Approval (first preference) [many entries]"
+ end
+ elseif approval_index == 2 then
+ if #entries == 1 then
+ heading = _"Approval (second preference) [single entry]"
+ else
+ heading = _"Approval (second preference) [many entries]"
+ end
+ elseif approval_index == 3 then
+ if #entries == 1 then
+ heading = _"Approval (third preference) [single entry]"
+ else
+ heading = _"Approval (third preference) [many entries]"
+ end
+ else
+ if #entries == 1 then
+ heading = _"Approval (#th preference) [single entry]"
+ else
+ heading = _"Approval (#th preference) [many entries]"
+ end
+ end
else
- heading = _"Approval (first preference) [many entries]"
- end
- elseif approval_index == 2 then
- if #entries == 1 then
- heading = _"Approval (second preference) [single entry]"
- else
- heading = _"Approval (second preference) [many entries]"
+ if #entries == 1 then
+ heading = _"Approval [single entry]"
+ else
+ heading = _"Approval [many entries]"
+ end
end
- elseif approval_index == 3 then
- if #entries == 1 then
- heading = _"Approval (third preference) [single entry]"
+ elseif class == "abstention" then
+ if #entries == 1 then
+ heading = _"Abstention [single entry]"
+ else
+ heading = _"Abstention [many entries]"
+ end
+ elseif class == "disapproval" then
+ disapproval_used = true
+ disapproval_index = disapproval_index + 1
+ if disapproval_count > disapproval_index + 1 then
+ if #entries == 1 then
+ heading = _"Disapproval (prefer to lower blocks) [single entry]"
+ else
+ heading = _"Disapproval (prefer to lower blocks) [many entries]"
+ end
+ elseif disapproval_count == 2 and disapproval_index == 1 then
+ if #entries == 1 then
+ heading = _"Disapproval (prefer to lower block) [single entry]"
+ else
+ heading = _"Disapproval (prefer to lower block) [many entries]"
+ end
+ elseif disapproval_index == disapproval_count - 1 then
+ if #entries == 1 then
+ heading = _"Disapproval (prefer to last block) [single entry]"
+ else
+ heading = _"Disapproval (prefer to last block) [many entries]"
+ end
else
- heading = _"Approval (third preference) [many entries]"
- end
- else
- if #entries == 1 then
- heading = _"Approval (#th preference) [single entry]"
- else
- heading = _"Approval (#th preference) [many entries]"
+ if #entries == 1 then
+ heading = _"Disapproval [single entry]"
+ else
+ heading = _"Disapproval [many entries]"
+ end
end
end
- else
- if #entries == 1 then
- heading = _"Approval [single entry]"
- else
- heading = _"Approval [many entries]"
- end
- end
- elseif class == "abstention" then
- if #entries == 1 then
- heading = _"Abstention [single entry]"
- else
- heading = _"Abstention [many entries]"
- end
- elseif class == "disapproval" then
- disapproval_used = true
- disapproval_index = disapproval_index + 1
- if disapproval_count > disapproval_index + 1 then
- if #entries == 1 then
- heading = _"Disapproval (prefer to lower blocks) [single entry]"
- else
- heading = _"Disapproval (prefer to lower blocks) [many entries]"
- end
- elseif disapproval_count == 2 and disapproval_index == 1 then
- if #entries == 1 then
- heading = _"Disapproval (prefer to lower block) [single entry]"
- else
- heading = _"Disapproval (prefer to lower block) [many entries]"
- end
- elseif disapproval_index == disapproval_count - 1 then
- if #entries == 1 then
- heading = _"Disapproval (prefer to last block) [single entry]"
- else
- heading = _"Disapproval (prefer to last block) [many entries]"
- end
- else
- if #entries == 1 then
- heading = _"Disapproval [single entry]"
- else
- heading = _"Disapproval [many entries]"
- end
- end
- end
- ui.tag {
- tag = "div",
- attr = { class = "cathead" },
- content = heading
- }
- for i, initiative in ipairs(entries) do
- ui.container{
- attr = {
- class = "movable",
- id = "entry_" .. tostring(initiative.id)
- },
- content = function()
- local initiators_selector = initiative:get_reference_selector("initiating_members")
- :add_where("accepted")
- local initiators = initiators_selector:exec()
- local initiator_names = {}
- for i, initiator in ipairs(initiators) do
- initiator_names[#initiator_names+1] = initiator.name
- end
- local initiator_names_string = table.concat(initiator_names, ", ")
+ ui.tag {
+ tag = "div",
+ attr = { class = "cathead " },
+ content = heading
+ }
+ for i, initiative in ipairs(entries) do
ui.container{
- attr = { style = "float: right; position: relative;" },
+ attr = {
+ class = "movable",
+ id = "entry_" .. tostring(initiative.id)
+ },
content = function()
- ui.link{
- attr = { class = "clickable" },
- content = _"Show",
- module = "initiative",
- view = "show",
- id = initiative.id
- }
- slot.put(" ")
- ui.link{
- attr = { class = "clickable", target = "_blank" },
- content = _"(new window)",
- module = "initiative",
- view = "show",
- id = initiative.id
+ local initiators_selector = initiative:get_reference_selector("initiating_members")
+ :add_where("accepted")
+ local initiators = initiators_selector:exec()
+ local initiator_names = {}
+ for i, initiator in ipairs(initiators) do
+ initiator_names[#initiator_names+1] = initiator.name
+ end
+ local initiator_names_string = table.concat(initiator_names, ", ")
+ ui.container{
+ attr = { style = "float: right; position: relative;" },
+ content = function()
+ ui.link{
+ attr = { class = "clickable" },
+ content = _"Show",
+ module = "initiative",
+ view = "show",
+ id = initiative.id
+ }
+ slot.put(" ")
+ ui.link{
+ attr = { class = "clickable", target = "_blank" },
+ content = _"(new window)",
+ module = "initiative",
+ view = "show",
+ id = initiative.id
+ }
+ if not readonly then
+ slot.put(" ")
+ ui.image{ attr = { class = "grabber" }, static = "icons/grabber.png" }
+ end
+ end
}
if not readonly then
- slot.put(" ")
- ui.image{ attr = { class = "grabber" }, static = "icons/grabber.png" }
- end
- end
- }
- if not readonly then
- ui.container{
- attr = { style = "float: left; position: relative;" },
- content = function()
- ui.tag{
- tag = "input",
- attr = {
- onclick = "if (jsFail) return true; voting_moveUp(this.parentNode.parentNode); return(false);",
- name = "move_up_" .. tostring(initiative.id),
- class = not disabled and "clickable" or nil,
- type = "image",
- src = encode.url{ static = "icons/move_up.png" },
- alt = _"Move up"
- }
- }
- slot.put(" ")
- ui.tag{
- tag = "input",
- attr = {
- onclick = "if (jsFail) return true; voting_moveDown(this.parentNode.parentNode); return(false);",
- name = "move_down_" .. tostring(initiative.id),
- class = not disabled and "clickable" or nil,
- type = "image",
- src = encode.url{ static = "icons/move_down.png" },
- alt = _"Move down"
- }
+ ui.container{
+ attr = { style = "float: left; position: relative;" },
+ content = function()
+ ui.tag{
+ tag = "button",
+ attr = {
+ onclick = "if (jsFail) return true; voting_moveUp(this.parentNode.parentNode); return(false);",
+ name = "move_up_" .. tostring(initiative.id),
+ class = "clickable mdl-button mdl-js-button mdl-button--icon",
+ alt = _"Move up",
+ },
+ content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "arrow_upward" }
+ end
+ }
+ ui.tag{
+ tag = "button",
+ attr = {
+ onclick = "if (jsFail) return true; voting_moveDown(this.parentNode.parentNode); return(false);",
+ name = "move_down_" .. tostring(initiative.id),
+ class = "clickable mdl-button mdl-js-button mdl-button--icon",
+ alt = _"Move down"
+ },
+ content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = "arrow_downward" }
+ end
+ }
+ slot.put(" ")
+ end
}
- slot.put(" ")
end
- }
- end
- ui.container{
- content = function()
- ui.tag{ content = "i" .. initiative.id .. ": " }
- ui.tag{ content = initiative.shortened_name }
- slot.put(" ")
- for i, initiator in ipairs(initiators) do
- ui.link{
- attr = { class = "clickable" },
- content = function ()
- execute.view{
- module = "member_image",
- view = "_show",
- params = {
- member = initiator,
- image_type = "avatar",
- show_dummy = true,
- class = "micro_avatar",
- popup_text = text
- }
+ ui.container{
+ content = function()
+ ui.tag{ attr = { class = "initiative_name" }, content = function()
+ ui.tag{ content = "i" .. initiative.id .. ": " }
+ ui.tag{ content = initiative.shortened_name }
+ end }
+ slot.put(" ")
+ for i, initiator in ipairs(initiators) do
+ ui.link{
+ attr = { class = "clickable" },
+ content = function ()
+ execute.view{
+ module = "member_image",
+ view = "_show",
+ params = {
+ member = initiator,
+ image_type = "avatar",
+ show_dummy = true,
+ class = "micro_avatar",
+ popup_text = text
+ }
+ }
+ end,
+ module = "member", view = "show", id = initiator.id
}
- end,
- module = "member", view = "show", id = initiator.id
- }
- slot.put(" ")
- ui.tag{ content = initiator.name }
- slot.put(" ")
- end
+ slot.put(" ")
+ ui.tag{ content = initiator.name }
+ slot.put(" ")
+ end
+ end
+ }
end
}
end
- }
- end
+ end
+ }
+ end
+ end
+ end
+ }
+ if app.session.member_id and preview then
+ local formatting_engine = param.get("formatting_engine") or config.enforce_formatting_engine
+ local comment = param.get("comment")
+ if comment and #comment > 0 then
+ local rendered_comment = format.wiki_text(comment, formatting_engine)
+ ui.container{ content = _"Voting comment" }
+ ui.container { attr = { class = "member_statement" }, content = function()
+ slot.put(rendered_comment)
+ end }
+ slot.put(" ")
+ end
+ end
+ if (readonly or direct_voter and direct_voter.comment) and not preview and not (app.session.member_id == member.id) then
+ local text
+ if direct_voter and direct_voter.comment_changed then
+ text = _("Voting comment (last updated: #{timestamp})", { timestamp = format.timestamp(direct_voter.comment_changed) })
+ elseif direct_voter and direct_voter.comment then
+ text = _"Voting comment"
+ end
+ if text then
+ ui.container{ content = text }
+ end
+ if direct_voter and direct_voter.comment then
+ local rendered_comment = direct_voter:get_content('html')
+ ui.container { attr = { class = "member_statement" }, content = function()
+ slot.put(rendered_comment)
+ end }
+ slot.put(" ")
+ end
+ end
+ if app.session.member_id and app.session.member_id == member.id then
+ if (not readonly or direct_voter) and not preview then
+ ui.container{ content = function()
+ if not config.enforce_formatting_engine then
+ ui.field.select{
+ label = _"Wiki engine for statement",
+ name = "formatting_engine",
+ foreign_records = config.formatting_engines,
+ attr = {id = "formatting_engine"},
+ foreign_id = "id",
+ foreign_name = "name",
+ value = param.get("formatting_engine") or direct_voter and direct_voter.formatting_engine
+ }
end
- }
+ ui.container{ content = _"Voting comment (optional)" }
+ ui.field.text{
+ name = "comment",
+ multiline = true,
+ value = param.get("comment") or direct_voter and direct_voter.comment,
+ attr = { style = "height: 10ex; width: 100%;" },
+ }
+ end }
+ end
+
+ if preview then
+ if not config.enforce_formatting_engine then
+ ui.field.hidden{ name = "formatting_engine", value = param.get("formatting_engine") }
+ end
+ ui.field.hidden{ name = "comment", value = param.get("comment") or direct_voter and direct_voter.comment }
+ end
+
+ if not readonly or direct_voter or preview then
+ if preview then
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ name = issue.closed and "update_comment" or nil,
+ value = submit_button_text -- finish voting / update comment
+ }
+ }
+ end
+ if not preview then
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "preview",
+ class = "mdl-button mdl-js-button mdl-button--raised mdl-button--colored",
+ value = _"Preview",
+ }
+ }
+ else
+ slot.put(" ")
+ ui.tag{
+ tag = "input",
+ attr = {
+ type = "submit",
+ name = "edit",
+ class = "mdl-button mdl-js-button mdl-button--raised",
+ value = edit_button_text,
+ }
+ }
+ end
end
end
end
}
- if app.session.member_id and preview then
- local formatting_engine = param.get("formatting_engine") or config.enforce_formatting_engine
- local comment = param.get("comment")
- if comment and #comment > 0 then
- local rendered_comment = format.wiki_text(comment, formatting_engine)
- ui.heading{ level = "2", content = _"Voting comment" }
- ui.container { attr = { class = "member_statement" }, content = function()
- slot.put(rendered_comment)
- end }
- slot.put(" ")
- end
- end
- if (readonly or direct_voter and direct_voter.comment) and not preview and not (app.session.member_id == member.id) then
- local text
- if direct_voter and direct_voter.comment_changed then
- text = _("Voting comment (last updated: #{timestamp})", { timestamp = format.timestamp(direct_voter.comment_changed) })
- elseif direct_voter and direct_voter.comment then
- text = _"Voting comment"
- end
- if text then
- ui.heading{ level = "2", content = text }
- end
- if direct_voter and direct_voter.comment then
- local rendered_comment = direct_voter:get_content('html')
- ui.container { attr = { class = "member_statement" }, content = function()
- slot.put(rendered_comment)
- end }
- slot.put(" ")
- end
- end
- if app.session.member_id and app.session.member_id == member.id then
- if (not readonly or direct_voter) and not preview then
- ui.container{ content = function()
- if not config.enforce_formatting_engine then
- ui.field.select{
- label = _"Wiki engine for statement",
- name = "formatting_engine",
- foreign_records = config.formatting_engines,
- attr = {id = "formatting_engine"},
- foreign_id = "id",
- foreign_name = "name",
- value = param.get("formatting_engine") or direct_voter and direct_voter.formatting_engine
- }
- end
- ui.heading { level = 2, content = _"Voting comment (optional)" }
- ui.field.text{
- name = "comment",
- multiline = true,
- value = param.get("comment") or direct_voter and direct_voter.comment,
- attr = { style = "height: 10ex; width: 100%;" },
+ slot.put(" ")
+ ui.link{
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised" },
+ text = _"Cancel",
+ module = "issue",
+ view = "show",
+ id = issue.id
+ }
+ if direct_voter then
+ slot.put(" ")
+ ui.link {
+ attr = { class = "mdl-button mdl-js-button mdl-button--raised" },
+ module = "vote", action = "update",
+ params = {
+ issue_id = issue.id,
+ discard = true
+ },
+ routing = {
+ default = {
+ mode = "redirect",
+ module = "issue",
+ view = "show",
+ id = issue.id
}
- end }
- end
-
- if preview then
- if not config.enforce_formatting_engine then
- ui.field.hidden{ name = "formatting_engine", value = param.get("formatting_engine") }
- end
- ui.field.hidden{ name = "comment", value = param.get("comment") or direct_voter and direct_voter.comment }
- end
-
- if not readonly or direct_voter or preview then
- ui.container{ content = function()
- if preview then
- slot.put(" ")
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- class = "btn btn-default",
- name = issue.closed and "update_comment" or nil,
- value = submit_button_text -- finish voting / update comment
- }
- }
- end
- if not preview then
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- name = "preview",
- class = "btn btn-default",
- value = _"Preview",
- }
- }
- else
- slot.put(" ")
- ui.tag{
- tag = "input",
- attr = {
- type = "submit",
- name = "edit",
- class = "btn-link",
- value = edit_button_text,
- }
- }
- end
- end }
- end
+ },
+ text = _"Discard my vote"
+ }
end
- end
- }
- slot.put(" ")
- ui.link{
- text = _"Cancel",
- module = "issue",
- view = "show",
- id = issue.id
- }
- if direct_voter then
- slot.put(" | ")
- ui.link {
- module = "vote", action = "update",
- params = {
- issue_id = issue.id,
- discard = true
- },
- routing = {
- default = {
- mode = "redirect",
- module = "issue",
- view = "show",
- id = issue.id
- }
- },
- text = _"Discard my vote"
- }
- end
-
- end )
-end )
\ No newline at end of file
+
+ end }
+ end }
+ end }
+end }
diff -r 7ea154c9238a -r 32cc544d5a5b app/main/vote/show_incoming.lua
--- a/app/main/vote/show_incoming.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/app/main/vote/show_incoming.lua Sun Jul 15 14:07:29 2018 +0200
@@ -8,6 +8,13 @@
issue = Issue:by_id(param.get("issue_id"))
end
+if not issue then
+ execute.view { module = "index", view = "404" }
+ request.set_status("404 Not Found")
+ return
+end
+
+
if app.session.member_id then
if initiative then
initiative:load_everything_for_member_id(app.session.member.id)
diff -r 7ea154c9238a -r 32cc544d5a5b config/example.lua
--- a/config/example.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/config/example.lua Sun Jul 15 14:07:29 2018 +0200
@@ -88,7 +88,7 @@
-- "everything"
-- -> Show everything a member can see, including profile pages
-- ------------------------------------------------------------------------
-config.public_access = "none"
+config.public_access = "authors_pseudonymous"
@@ -483,5 +483,28 @@
-- ------------------------------------------------------------------------
-- uncomment the following line to enable debug trace
-- ------------------------------------------------------------------------
--- config.enable_debug_trace = true
+config.enable_debug_trace = true
+
+
+config.fork = {
+ pre =1, min = 1, max = 1, max_requests = 1, min_requests = 1
+}
+
+config.localhost = true
+config.oauth2 = {
+ available_scopes = {
+ { scope = "read", name = { de = "Lesen", en = "Read data" } },
+ { scope = "write", name = { de = "Schreiben", en = "Write data" } },
+ { scope = "privA", name = { de = "Beispielprivileg A", en = "Example privilege A" } },
+ { scope = "privB", name = { de = "Beispielprivileg B", en = "Example privilege B" } }
+ },
+ authorization_code_lifetime = 5 * 60,
+ refresh_token_lifetime = 60 * 60 * 24 * 30 * 3,
+ refresh_pause = 60,
+ refresh_grace_period = 60,
+ access_token_lifetime = 60 * 60,
+ -- NOTE for init.lua : check for refresh_pause >= refresh_grace_period
+ endpoint_magic = "liquidfeedback_client_redirection_endpoint"
+}
+
diff -r 7ea154c9238a -r 32cc544d5a5b env/format/interval_text.lua
--- a/env/format/interval_text.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/env/format/interval_text.lua Sun Jul 15 14:07:29 2018 +0200
@@ -5,6 +5,8 @@
local options = options or {}
+ value = value:match("^([^ ]* *[^ ]* *[^ ]* *[^ ]*)")
+
value = value:gsub("%..*", "")
:gsub("days", "{DAYS}")
:gsub("day", "{DAY}")
diff -r 7ea154c9238a -r 32cc544d5a5b env/lf4rcs/commit.lua
--- a/env/lf4rcs/commit.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/env/lf4rcs/commit.lua Sun Jul 15 14:07:29 2018 +0200
@@ -42,7 +42,7 @@
local target_node_id = initiative.current_draft.external_reference
if target_node_id then
local branch = "i" .. initiative.id
- lf4rcs[repository].commit(path, exec, branch, target_node_id, close_message, merge_message)
+ config.lf4rcs[repository].commit(path, exec, branch, target_node_id, close_message, merge_message)
end
end
end
diff -r 7ea154c9238a -r 32cc544d5a5b env/lf4rcs/init.lua
--- a/env/lf4rcs/init.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/env/lf4rcs/init.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,9 +1,7 @@
function lf4rcs.init()
- local super_handler = config.notification_handler_func
- config.notification_handler_func = function(event)
- if super_handler then super_handler(event) end
+ Event.add_handler(function(event)
lf4rcs.notification_handler(event)
- end
+ end)
config.render_external_reference = {
draft = lf4rcs.render_draft_reference,
initiative = lf4rcs.render_initiative_reference
diff -r 7ea154c9238a -r 32cc544d5a5b env/model/has_rendered_content.lua
--- a/env/model/has_rendered_content.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/env/model/has_rendered_content.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,4 +1,4 @@
-function model.has_rendered_content(class, rendered_class, content_field_name)
+function model.has_rendered_content(class, rendered_class, content_field_name, primary_key)
local content_field_name = content_field_name or 'content'
@@ -12,6 +12,8 @@
for i, key in ipairs(class.primary_key) do
selector:add_where{ "$ = ?", { key }, self[key] }
end
+ elseif class.primary_key then
+ selector:add_where{ "$ = ?", { class.primary_key }, self[class.primary_key] }
else
selector:add_where{ "id = ?", self.id }
end
@@ -24,6 +26,8 @@
for i, key in ipairs(class.primary_key) do
selector:add_where{ "$.$ = ?", { rendered_class.table }, { key }, self[key] }
end
+ elseif class.primary_key then
+ selector:add_where{ "$." .. primary_key .. " = ?", { rendered_class.table }, self[class.primary_key] }
else
selector:add_where{ "$." .. class.table .. "_id = ?", { rendered_class.table }, self.id }
end
@@ -43,6 +47,8 @@
for i, key in ipairs(class.primary_key) do
rendered[key] = self[key]
end
+ elseif class.primary_key then
+ rendered[primary_key] = self[class.primary_key]
else
rendered[class.table .. "_id"] = self.id
end
@@ -65,6 +71,8 @@
for i, key in ipairs(class.primary_key) do
selector:add_where{ "$.$ = ?", { rendered_class.table }, { key }, self.id }
end
+ elseif class.primary_key then
+ selector:add_where{ primary_key .. " = ?", self[class.primary_key] }
else
selector:add_where{ class.table .. "_id = ?", self.id }
end
@@ -79,4 +87,4 @@
return rendered.content
end
-end
\ No newline at end of file
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/request/router.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/request/router.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,41 @@
+local api_endpoints = {
+ instance = true,
+ navigation = true,
+ style = true,
+ application = true,
+ info = true,
+ member = true,
+ notify_email = true,
+ profile_info = true,
+ profile = true,
+ settings_info = true,
+ settings = true,
+ event = true
+}
+
+function request.router()
+
+ local api_prefix = "api/1/"
+
+ local path = request.get_path()
+
+ if path == api_prefix .. "register" then
+ return { module = "oauth2", view = "register" }
+ elseif path == api_prefix .. "authorization" then
+ return { module = "oauth2", view = "authorization" }
+ elseif path == api_prefix .. "token" then
+ return { module = "oauth2", view = "token" }
+ elseif path == api_prefix .. "validate" then
+ return { module = "oauth2", view = "validate" }
+ elseif path == api_prefix .. "session" then
+ return { module = "oauth2", view = "session" }
+ else
+ local endpoint = string.match(path, "^" .. api_prefix .. "(.*)$")
+ if api_endpoints[endpoint] then
+ return { module = "api", view = endpoint }
+ end
+ end
+
+ return request.default_router(path)
+
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/bargraph.lua
--- a/env/ui/bargraph.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/env/ui/bargraph.lua Sun Jul 15 14:07:29 2018 +0200
@@ -26,7 +26,7 @@
if bar.value > 0 then
at_least_one_bar = true
local value = bar.value * args.width / args.max_value
- if quorum and quorum < length + value then
+ if quorum and quorum <= length + value then
local dlength = math.max(quorum - length - 1, 0)
local dlength_abs = math.floor(dlength)
local rest = rest + dlength - dlength_abs
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/cell_full.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/ui/cell_full.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,6 @@
+function ui.cell_full(args)
+ args.attr = args.attr or {}
+ args.attr.class = args.attr.class and args.attr.class .. " " or ""
+ args.attr.class = args.attr.class .. "mdl-cell mdl-cell--12-col"
+ ui.container(args)
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/cell_main.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/ui/cell_main.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,6 @@
+function ui.cell_main(args)
+ args.attr = args.attr or {}
+ args.attr.class = args.attr.class and args.attr.class .. " " or ""
+ args.attr.class = args.attr.class .. "mdl-cell mdl-cell--12-col mdl-cell--5-col-tablet mdl-cell--8-col-desktop mdl-cell--order-2-tablet mdl-cell--order-2-desktop"
+ ui.container(args)
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/cell_sidebar.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/ui/cell_sidebar.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,6 @@
+function ui.cell_sidebar(args)
+ args.attr = args.attr or {}
+ args.attr.class = args.attr.class and args.attr.class .. " " or ""
+ args.attr.class = args.attr.class .. "mdl-cell mdl-cell--12-col mdl-cell--3-col-tablet mdl-cell--4-col-desktop mdl-cell--order-1-tablet mdl-cell--order-1-desktop"
+ ui.container(args)
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/field/location.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/ui/field/location.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,33 @@
+function ui.field.location(args)
+ if config.map then
+ ui.form_element(args, {fetch_value = true}, function(args)
+ ui.tag{
+ tag = "input",
+ attr = { type = "hidden", name = args.name, value = args.value, id = "ui_field_location_value" }
+ }
+ end)
+ ui.map({}, "ui_field_location_value")
+ elseif config.firstlife then
+ ui.form_element(args, {fetch_value = true}, function(args)
+ ui.tag{
+ tag = "input",
+ attr = { type = "hidden", name = args.name, value = args.value, id = "ui_field_location_value" }
+ }
+ ui.tag{ tag = "iframe", attr = { src = config.firstlife.inputmap_url .. "/src/index.html?domain=" .. request.get_absolute_baseurl() .. "&" .. config.firstlife.coordinates .. "&lightArea=false&contrast=false&mode=lite", id = "ui_field_location", class = "ui_field_location" }, content = "" }
+
+ ui.script{ script = [[
+
+ window.addEventListener("message", function (e) {
+ if (e.origin !== "]] .. config.firstlife.inputmap_url .. [[") return;
+ var data = e.data;
+ if (data.src == "InputMap") {
+ var el = document.getElementById("ui_field_location_value");
+ el.value = JSON.stringify({ "type": "Point", "coordinates": [data.lng, data.lat], "zoom_level": data.zoom_level });
+ console.log(el.value);
+ }
+ });
+
+ ]] }
+ end)
+ end
+end
diff -r 7ea154c9238a -r 32cc544d5a5b env/ui/field/wysihtml.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/env/ui/field/wysihtml.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,83 @@
+function ui.field.wysihtml(args)
+
+ local toolbar = {
+ { command = "bold", title ="CTRL+B", icon = "format_bold" },
+ { command = "italic", title ="CTRL+I", icon = "format_italic" },
+ { command = "createLink", icon = "insert_link" },
+ { command = "removeLink", icon = "insert_link", crossed = "\\" },
+ { command = "formatBlock", command_value = "h1", icon = "title", head_level = "1" },
+ { command = "formatBlock", command_value = "h2", icon = "title", head_level = "2" },
+ { command = "formatBlock", command_value = "h3", icon = "title", head_level = "3" },
+ { command = "formatBlock", command_blank = "true", icon = "format_clear" },
+ { command = "insertBlockQuote", icon = "format_quote" },
+ { command = "insertUnorderedList", icon = "format_list_bulleted" },
+ { command = "insertOrderedList", icon = "format_list_numbered" },
+ { command = "outdentList", icon = "format_indent_decrease" },
+ { command = "indentList", icon = "format_indent_increase" },
+-- { command = "alignLeftStyle", icon = "format_align_left" },
+-- { command = "alignRightStyle", icon = "format_align_right" },
+-- { command = "alignCenterStyle", icon = "format_align_center" },
+ { command = "undo", icon = "undo" },
+ { command = "redo", icon = "redo" }
+ }
+
+ slot.put([[
+
+ ]])
+
+ ui.container{ attr = { id = "toolbar", class = "toolbar", style = "display: none;" }, content = function()
+ for i, t in ipairs(toolbar) do
+ ui.tag{ tag = "a", attr = { ["data-wysihtml-command"] = t.command, ["data-wysihtml-command-value"] = t.command_value, ["data-wysihtml-command-blank-value"] = t.command_blank, title = t.shortcut }, content = function()
+ ui.tag{ tag = "i", attr = { class = "material-icons" }, content = t.icon }
+ if t.crossed then
+ ui.tag{ attr = { class = "crossed" }, content = t.crossed }
+ end
+ if t.head_level then
+ ui.tag{ attr = { class = "head_level" }, content = t.head_level }
+ end
+ end }
+ end
+ slot.put([[
+
[^<>]*", normalize_whitespace)
+ return str
+end
diff -r 7ea154c9238a -r 32cc544d5a5b lib/ontomap/ontomap.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/ontomap/ontomap.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,382 @@
+function _G.ontomap_get_instances(event)
+ if true then return {} end
+ local url = config.ontomap.base_url .. "instances/Issue?geometries=true&descriptions=true"
+ print("OnToMap>>")
+
+ local output, err, status = extos.pfilter(doc, "curl", "-X", "GET", "-H", "Content-Type: application/json", "--cert", config.ontomap.client_cert_file, "-d", "@-", url)
+ print(output)
+
+ local data = json.import(output)
+
+ if not data then
+ return {}
+ end
+
+ if data.type ~= "FeatureCollection" or not data.features then
+ return {}
+ end
+
+ local instances = {}
+ for i, feature in ipairs(data.features) do
+ if feature.geometry then
+ table.insert(instances, {
+ application = feature.applicationName,
+ title = feature.hasName,
+ description = slot.use_temporary(function()
+ ui.link{ external = feature.properties.external_url, text = feature.properties.hasName or "" }
+ ui.container{ content = feature.hasDescription }
+ end),
+ lon = feature.geometry.coordinates[1],
+ lat = feature.geometry.coordinates[2],
+ label = "IMC" .. feature.properties.hasID,
+ type = "Improve My City"
+ })
+ print(feature.applicationName, feature.properties.hasName, feature.properties.hasDescription)
+ end
+ end
+ return instances
+
+end
+
+
+local function new_log_event(event, actor, activity_type)
+ local e = json.object{
+ activity_type = activity_type,
+ activity_objects = json.array(),
+ references = json.array(),
+ visibility_details = json.array(),
+ details = json.object()
+ }
+ e.actor = actor
+ e.timestamp = math.floor(event.occurrence_epoch * 1000)
+ return e
+end
+
+local function new_activity_object(attr)
+ return json.object{
+ type = "Feature",
+ geometry = attr.geometry or json.null,
+ properties = json.object{
+ hasType = attr.type,
+ external_url = attr.url
+ }
+ }
+end
+
+local function new_reference_object(url, application)
+ return json.object{
+ application = application or config.ontomap.application_ident,
+ external_url = url
+ }
+end
+
+local function log_to_ontomap(log_events)
+ if json.type(log_events) == "object" then
+ log_events = json.array{ log_events }
+ end
+ for i, log_event in ipairs(log_events) do
+ if #(log_event.activity_objects) == 0 then
+ log_event.activity_objects = nil
+ end
+ if #(log_event.references) == 0 then
+ log_event.references = nil
+ end
+ if #(log_event.visibility_details) == 0 then
+ log_event.visibility_details = nil
+ end
+ if not (next(log_event.details)) then -- TODO
+ log_event.details = nil
+ end
+ end
+
+ local doc = json.export(json.object{
+ event_list = log_events
+ }, " ")
+
+ local url = config.ontomap.base_url .. "logger/events"
+ print("OnToMap<<")
+ print(doc)
+
+ local output, err, status = extos.pfilter(doc, "curl", "-X", "POST", "-H", "Content-Type: application/json", "--cert", config.ontomap.client_cert_file, "-d", "@-", url)
+
+ print("---------")
+ print(output)
+ print("---------")
+
+ if err then
+ -- TODO log error
+ end
+end
+
+local function url_for(relation, id)
+ return config.absolute_base_url .. relation .. "/show/" .. id .. ".html"
+end
+
+
+local function unit_updated(event, event_type)
+ local log_event = new_log_event(event, 0, event_type == "created" and "object_created" or "object_updated")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "unit",
+ url = url_for("unit", event.unit_id)
+ })
+ log_event.details.active = event.unit.active
+ log_event.details.name = event.unit.name
+ log_event.details.description = event.unit.description
+ log_event.details.external_reference = event.unit.external_reference
+ log_event.details.region = event.unit.region
+ log_to_ontomap(log_event)
+end
+
+local function area_updated(event, event_type)
+ local log_event = new_log_event(event, 0, event_type == "created" and "object_created" or "object_updated")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "area",
+ url = url_for("area", event.area_id)
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("unit", event.area.unit_id)
+ ))
+ log_event.details.active = event.area.active
+ log_event.details.name = event.area.name
+ log_event.details.description = event.area.description
+ log_event.details.external_reference = event.area.external_reference
+ log_event.details.region = event.area.region
+ log_to_ontomap(log_event)
+end
+
+local function policy_updated(event, event_type)
+ local log_event = new_log_event(event, 0, event_type == "created" and "object_created" or "object_updated")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "policy",
+ url = url_for("policy", event.policy_id)
+ })
+ log_event.details.active = event.policy.active
+ log_event.details.name = event.policy.name
+ log_event.details.description = event.policy.description
+ log_to_ontomap(log_event)
+end
+
+local mapper = {
+
+ unit_created = function(event)
+ unit_updated(event, "created")
+ end,
+
+ unit_updated = function(event)
+ unit_updated(event, "updated")
+ end,
+
+ area_created = function(event)
+ area_updated(event, "created")
+ end,
+
+ area_updated = function(event)
+ area_updated(event, "updated")
+ end,
+
+ policy_created = function(event)
+ policy_updated(event, "created")
+ end,
+
+ policy_updated = function(event)
+ policy_updated(event, "updated")
+ end,
+
+ issue_state_changed = function(event)
+ local log_event = new_log_event(event, 0, "issue_status_updated")
+ table.insert(log_event.references, new_reference_object(
+ url_for("issue", event.issue_id)
+ ))
+ log_event.details.new_issue_state = event.state
+ log_to_ontomap(log_event)
+ end,
+
+ initiative_created_in_new_issue = function(event)
+ local log_events = json.array()
+
+ local log_event = new_log_event(event, 0, "object_created")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "issue",
+ url = url_for("issue", event.issue_id)
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("policy", event.issue.policy_id)
+ ))
+ log_event.details.new_issue_state = event.state
+ table.insert(log_events, log_event)
+
+ local log_event = new_log_event(event, event.member_id, "object_created")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "initiative",
+ url = url_for("initiative", event.initiative_id),
+ geometry = event.initiative.location
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("issue", event.issue_id)
+ ))
+ log_event.details.name = event.initiative.name
+ table.insert(log_events, log_event)
+
+ log_to_ontomap(log_events)
+ end,
+
+ initiative_created_in_existing_issue = function(event)
+ local log_event = new_log_event(event, event.member_id, "object_created")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "initiative",
+ url = url_for("initiative", event.initiative_id),
+ geometry = event.initiative.location
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("issue", event.issue_id)
+ ))
+ log_event.details.name = event.initiative.name
+ log_to_ontomap(log_event)
+ end,
+
+ initiative_revoked = function(event)
+ -- TODO -> which activity?
+ end,
+
+ new_draft_created = function(event)
+ local log_event = new_log_event(event, event.member_id, "object_updated")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "initiative",
+ url = url_for("initiative", event.issue_id)
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("issue", event.issue_id)
+ ))
+ log_event.details.name = event.initiative.name
+ log_event.details.location = event.initiative.current_draft.location
+ log_to_ontomap(log_event)
+ end,
+
+ interest = function(event)
+ local activity_type = event.boolean_value and "interest_added" or "interest_removed"
+ local log_event = new_log_event(event, event.member_id, activity_type)
+ table.insert(log_event.references, new_reference_object(
+ url_for("issue", event.issue_id)
+ ))
+ log_to_ontomap(log_event)
+ end,
+
+ initiator = function(event)
+ local activity_type = event.boolean_value and "initiator_added" or "initiator_removed"
+ local log_event = new_log_event(event, event.member_id, activity_type)
+ table.insert(log_event.references, new_reference_object(
+ url_for("initiative", event.initiative_id)
+ ))
+ log_to_ontomap(log_event)
+ end,
+
+ support = function(event)
+ local activity_type = event.boolean_value and "support_added" or "support_removed"
+ local log_event = new_log_event(event, event.member_id, activity_type)
+ table.insert(log_event.references, new_reference_object(
+ url_for("initiative", event.initiative_id)
+ ))
+ log_event.details.draft_id = event.draft_id
+ log_to_ontomap(log_event)
+ end,
+
+ support_updated = function(event)
+ local log_event = new_log_event(event, event.member_id, "support_updated")
+ table.insert(log_event.references, new_reference_object(
+ url_for("initiative", event.initiative_id)
+ ))
+ log_event.details.draft_id = event.draft_id
+ log_to_ontomap(log_event)
+ end,
+
+ suggestion_created = function(event)
+ local log_event = new_log_event(event, event.member_id, "object_created")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "suggestion",
+ url = url_for("suggestion", event.suggestion_id)
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("initiative", event.initiative_id)
+ ))
+ log_to_ontomap(log_event)
+ end,
+
+ suggestion_removed = function(event)
+ local log_event = new_log_event(event, 0, "object_removed")
+ table.insert(log_event.activity_objects, new_activity_object{
+ type = "suggestion",
+ url = url_for("suggestion", event.suggestion_id)
+ })
+ table.insert(log_event.references, new_reference_object(
+ url_for("initiative", event.initiative_id)
+ ))
+ log_to_ontomap(log_event)
+ end,
+
+ suggestion_rated = function(event)
+ local log_event = new_log_event(event, event.member_id, "suggestion_rated")
+ table.insert(log_event.references, new_reference_object(
+ url_for("suggestion", event.suggestion_id)
+ ))
+ log_event.details.degree = event.numeric_value
+ log_event.details.fulfilled = event.boolean_value or json.null
+ log_to_ontomap(log_event)
+ end,
+
+ delegation = function(event)
+ -- TODO
+ end,
+
+ member_activated = function(event)
+ local log_event = new_log_event(event, event.member_id, "account_registered")
+ log_to_ontomap(log_event)
+ end,
+
+ member_removed = function(event)
+ -- TODO -> which activity to log?
+ end,
+
+ member_active = function(event)
+ -- TODO -> which activity to log?
+ end,
+
+ member_name_updated = function(event)
+ local log_event = new_log_event(event, event.member_id, "screen_name_changed")
+ log_event.details.screen_name = event.text_value
+ log_to_ontomap(log_event)
+ end,
+
+ member_profile_updated = function(event)
+ local log_event = new_log_event(event, event.member_id, "profile_updated")
+ log_to_ontomap(log_event)
+ end,
+
+ member_image_updated = function(event)
+ local log_event = new_log_event(event, event.member_id, "avatar_changed")
+ log_to_ontomap(log_event)
+ end,
+
+ contact = function(event)
+ local activity_type = event.boolean_value and "contact_published" or "contact_unpublished"
+ local log_event = new_log_event(event, event.member_id, activity_type)
+ log_event.details.other_member_id = event.other_member_id
+ log_to_ontomap(log_event)
+ end
+
+}
+
+function _G.ontomap_log_event(event)
+
+ if mapper[event.event] then
+ local e = Event:new_selector()
+ :add_where{ "id = ?", event.id }
+ :add_field("extract(epoch from occurrence)", "occurrence_epoch")
+ :optional_object_mode()
+ :exec()
+ if e then
+ mapper[event.event](e)
+ end
+ end
+
+
+end
diff -r 7ea154c9238a -r 32cc544d5a5b locale/translations.de.lua
--- a/locale/translations.de.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/locale/translations.de.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,5 +1,6 @@
#!/usr/bin/env lua
return {
+["all subject areas"] = "Alle Themenbereiche";
[" to receive updates by email"] = " bearbeiten um Aktualisierungen per E-Mail zu erhalten";
["#{closed_ago} ago"] = "vor #{closed_ago}";
["#{count} Neutral"] = "#{count} Enthaltung";
@@ -33,7 +34,7 @@
["#{policy_name} ##{issue_id}"] = false;
["#{policy} ##{id}"] = false;
["#{result}: #{yes_count} Yes (#{yes_percent}), #{no_count} No (#{no_percent}), #{neutral_count} Abstention (#{neutral_percent})"] = "#{result}: #{yes_count} Ja (#{yes_percent}), #{no_count} Nein (#{no_percent}), #{neutral_count} Enthaltung (#{neutral_percent})";
-["#{result}: No votes (0)"] = "#{result}: Keine Stimmen (0)";
+["No votes (0)"] = "Keine Stimmen (0)";
["(+ #{count} potential)"] = "(+ #{count} potentielle)";
["(1) Admission"] = "(1) Zulassung";
["(1) Admission phase"] = "(1) Zulassungsphase";
@@ -425,7 +426,7 @@
["No matching members found"] = "Keine passenden Mitglieder gefunden";
["No more events available"] = "Keine weiteren Ereignisse verfügbar";
["No multistage majority"] = "Keine mehrstufigen Mehrheiten";
-["No published contacts"] = "Keiner Kontakte veröffentlicht";
+["No published contacts"] = "Keine Kontakte veröffentlicht";
["No results for this selection"] = "Keine Ergebnisse für diese Auswahl";
["No reverse beat path"] = "Kein rückwärtsgerichteter Schlagpfad";
["No suggestions"] = "Keine Verbesserungsvorschläge";
@@ -958,9 +959,9 @@
["supporter"] = "Unterstützer";
["supporter with restricting suggestions"] = "Unterstützer mit beschränkenden Verbesserungsvorschlägen";
["take a look at the competing initiatives"] = "schau dir die konkurrierenden Initiativen an";
-["take a look at the suggestions (see left) and rate them"] = "schau dir die Verbesserungsvorschläge (links) an und bewerte sie";
+["take a look at the suggestions (see right) and rate them"] = "schau dir die Verbesserungsvorschläge (rechts) an und bewerte sie";
["take a look at the suggestions of your supporters"] = "schau dir die Verbesserungsvorschläge deiner Unterstützer an";
-["take a look on the issues (see left)"] = "schau dir die Themen an (siehe links)";
+["take a look on the issues (see right)"] = "schau dir die Themen an (siehe rechts)";
["the following login is connected to this email address:\n\n"] = "der folgende Anmeldename ist mit dieser E-Mail-Adresse verknüpft:\n\n";
["this issue is in verification phase, therefore the initiative text cannot be updated anymore"] = "dieses Thema ist in der Überprüfungsphase, daher kann der Text nicht mehr geändert werden";
["this issue is in voting phase, therefore the initiative text cannot be updated anymore"] = "dieses Thema ist in Abstimmung, daher kann der Text nicht mehr geändert werden";
@@ -1007,4 +1008,8 @@
["you have #{count} incoming delegations"] = "#{count} eingehende Delegationen";
["you restricted your support by rating suggestions as must or must not"] = "deine Untersützung ist aufgrund von 'muss'- oder 'darf nicht'-Verbesserungsvorschlägen beschränkt";
["you voted"] = "du hast abgestimmt";
+["new issue"] = "Neues Thema starten";
+["expert editor (HTML)"] = "Expertenmodus (HTML)";
+["show profile"] = "Profil aufrufen";
+["show ballot"] = "Stimmzettel aufrufen";
}
diff -r 7ea154c9238a -r 32cc544d5a5b model/agent.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/agent.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,27 @@
+Agent= mondelefant.new_class()
+Agent.table = 'agent'
+Agent.primary_key = { "controlled_id", "controller_id" }
+
+Agent:add_reference{
+ mode = 'm1',
+ to = "Member",
+ this_key = 'controller_id',
+ that_key = 'id',
+ ref = 'controller',
+}
+
+Agent:add_reference{
+ mode = 'm1',
+ to = "Member",
+ this_key = 'controlled_id',
+ that_key = 'id',
+ ref = 'controllee',
+}
+
+function Agent:by_pk(controlled_id, controller_id)
+ return self:new_selector()
+ :add_where{ "controlled_id = ?", controlled_id }
+ :add_where{ "controller_id = ?", controller_id }
+ :optional_object_mode()
+ :exec()
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/direct_voter.lua
--- a/model/direct_voter.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/direct_voter.lua Sun Jul 15 14:07:29 2018 +0200
@@ -18,11 +18,11 @@
ref = 'member',
}
-model.has_rendered_content(DirectVoter, RenderedVoterComment, "comment")
+model.has_rendered_content(DirectVoter, RenderedVoterComment, "comment", { "issue_id", "member_id" })
function DirectVoter:by_pk(issue_id, member_id)
return self:new_selector()
:add_where{ "issue_id = ? AND member_id = ?", issue_id, member_id }
:optional_object_mode()
:exec()
-end
\ No newline at end of file
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/draft.lua
--- a/model/draft.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/draft.lua Sun Jul 15 14:07:29 2018 +0200
@@ -23,7 +23,7 @@
return self.author and self.author.name or _"Unknown author"
end
-model.has_rendered_content(Draft, RenderedDraft)
+model.has_rendered_content(Draft, RenderedDraft, "content", "draft_id")
function Draft:update_content(member_id, initiative_id, p_formatting_engine, content, external_reference, preview)
local initiative = Initiative:by_id(initiative_id)
@@ -44,7 +44,7 @@
local initiator = Initiator:by_pk(initiative.id, member_id)
if not initiator or not initiator.accepted then
- error("access denied")
+ return false
end
local tmp = db:query({ "SELECT text_entries_left FROM member_contingent_left WHERE member_id = ? AND polling = ?", member_id, initiative.polling }, "opt_object")
diff -r 7ea154c9238a -r 32cc544d5a5b model/dynamic_application_scope.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/dynamic_application_scope.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,73 @@
+DynamicApplicationScope = mondelefant.new_class()
+DynamicApplicationScope.table = 'dynamic_application_scope'
+DynamicApplicationScope.primary_key = { "redirect_uri", "flow", "scope" }
+
+function DynamicApplicationScope:by_redirect_uri_and_flow(redirect_uri, flow)
+ local dynamic_application_scopes = self:new_selector()
+ :add_where{ "redirect_uri = ?", redirect_uri }
+ :add_where{ "flow = ?", flow }
+ :add_where("expiry >= now()")
+ :exec()
+ return dynamic_application_scopes
+end
+
+function DynamicApplicationScope:check_scopes(domain, redirect_uri, requested_flow, requested_scopes)
+ local function check_scopes(permitted_scopes)
+ local missing_scope = false
+ for scope in pairs(requested_scopes) do
+ if not permitted_scopes[scope] then
+ missing_scope = true
+ end
+ end
+ return missing_scope
+ end
+
+ local registered = false
+ local missing_scope = false
+
+ local dynamic_application_scopes = DynamicApplicationScope:by_redirect_uri_and_flow(redirect_uri, requested_flow)
+
+ if #dynamic_application_scopes > 0 then
+ registered = true
+ local permitted_scopes = {}
+ for i, dynamic_application_scope in ipairs(dynamic_application_scopes) do
+ permitted_scopes[dynamic_application_scope.scope] = true
+ end
+ missing_scope = check_scopes(permitted_scopes)
+ end
+
+ if not registered or missing_scope then
+ local output, err, status = config.oauth2.host_func("_liquidfeedback_client." .. domain)
+ if output == nil then
+ error("Cannot execute host_func command")
+ end
+ if status == 0 then
+ for line in string.gmatch(output, "[^\r\n]+") do
+ local flow, result = string.match(line, '"dynamic client v1" "([^"]+)" (.+)$')
+ if flow == requested_flow then
+ registered = true
+ local permitted_scopes = {}
+ local wildcard = false
+ for entry in string.gmatch(result, '"([^"]+)"') do
+ if entry == "*" then
+ wildcard = true
+ break
+ end
+ permitted_scopes[entry] = true
+ end
+ if not wildcard then
+ missing_scope = check_scopes(permitted_scopes)
+ end
+ end
+ end
+ end
+ end
+
+ if not registered then
+ return "not_registered"
+ elseif missing_scope then
+ return "missing_scope"
+ else
+ return "ok"
+ end
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/event.lua
--- a/model/event.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/event.lua Sun Jul 15 14:07:29 2018 +0200
@@ -3,6 +3,30 @@
Event:add_reference{
mode = 'm1',
+ to = "Unit",
+ this_key = 'unit_id',
+ that_key = 'id',
+ ref = 'unit',
+}
+
+Event:add_reference{
+ mode = 'm1',
+ to = "Area",
+ this_key = 'area_id',
+ that_key = 'id',
+ ref = 'area',
+}
+
+Event:add_reference{
+ mode = 'm1',
+ to = "Policy",
+ this_key = 'policy_id',
+ that_key = 'id',
+ ref = 'policy',
+}
+
+Event:add_reference{
+ mode = 'm1',
to = "Issue",
this_key = 'issue_id',
that_key = 'id',
@@ -33,6 +57,13 @@
ref = 'member',
}
+function Event:by_member_id(member_id)
+ return Event:new_selector()
+ :add_where{ "member_id = ?", member_id }
+ :add_order_by("id DESC")
+ :exec()
+end
+
function Event.object_get:event_name()
return ({
issue_state_changed = _"Issue reached next phase",
@@ -136,40 +167,57 @@
end
-function Event:send_next_notification()
-
- local notification_event_sent = NotificationEventSent:new_selector()
- :optional_object_mode()
- :for_update()
- :exec()
-
- local last_event_id = 0
- if notification_event_sent then
- last_event_id = notification_event_sent.event_id
- end
+function Event:get_events_after_id(id)
+ return (
+ Event:new_selector()
+ :add_where{ "event.id > ?", id }
+ :add_order_by("event.id")
+ :exec()
+ )
+end
+
+
+
+Event.handlers = {}
+
+function Event:add_handler(func)
+ table.insert(Event.handlers, func)
+end
+
+function Event:process_stream(poll)
+
+ db:query('LISTEN "event"')
+
+ local last_event_id = EventProcessed:get_last_id()
- local event = Event:new_selector()
- :add_where{ "event.id > ?", last_event_id }
- :add_order_by("event.id")
- :limit(1)
- :optional_object_mode()
- :exec()
+ while true do
+
+ local events = Event:get_events_after_id(last_event_id)
+
+ for i_event, event in ipairs(events) do
+ last_event_id = event.id
+ EventProcessed:set_last_id(last_event_id)
+ for i_handler, event_handler in ipairs(Event.handlers) do
+ event_handler(event)
+ end
+ event:send_notification()
+ end
- if event then
- if last_event_id == 0 then
- db:query{ "INSERT INTO notification_event_sent (event_id) VALUES (?)", event.id }
+ if poll then
+ while not db:wait(0) do
+ if not poll({[db.fd]=true}, nil, nil, true) then
+ goto exit
+ end
+ end
else
- db:query{ "UPDATE notification_event_sent SET event_id = ?", event.id }
+ db:wait()
end
-
- event:send_notification()
+ while db:wait(0) do end -- discard multiple events
- if config.notification_handler_func then
- config.notification_handler_func(event)
- end
-
- return true
-
end
+ ::exit::
+
+ db:query('UNLISTEN "event"')
+
end
diff -r 7ea154c9238a -r 32cc544d5a5b model/event_processed.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/event_processed.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,22 @@
+EventProcessed = mondelefant.new_class()
+EventProcessed.table = 'event_processed'
+
+function EventProcessed:get_last_id()
+
+ local event_processed = self:new_selector()
+ :optional_object_mode()
+ :for_update()
+ :exec()
+
+ local last_event_id = 0
+ if event_processed then
+ last_event_id = event_processed.event_id
+ end
+
+ return last_event_id
+
+end
+
+function EventProcessed:set_last_id(id)
+ db:query{ "INSERT INTO event_processed (event_id) VALUES (?) ON CONFLICT ((1)) DO UPDATE SET event_id = EXCLUDED.event_id", id }
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/initiative.lua
--- a/model/initiative.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/initiative.lua Sun Jul 15 14:07:29 2018 +0200
@@ -154,9 +154,9 @@
selector:left_join("supporter", nil, "supporter.initiative_id = initiative.id AND supporter.member_id = delegation_info.participating_member_id")
- selector:left_join("delegating_interest_snapshot", "delegating_interest_s", { "delegating_interest_s.event = issue.latest_snapshot_event AND delegating_interest_s.issue_id = issue.id AND delegating_interest_s.member_id = ?", options.member_id })
+ selector:left_join("delegating_interest_snapshot", "delegating_interest_s", { "delegating_interest_s.snapshot_id = issue.latest_snapshot_id AND delegating_interest_s.issue_id = issue.id AND delegating_interest_s.member_id = ?", options.member_id })
- selector:left_join("direct_supporter_snapshot", "supporter_s", { "supporter_s.event = issue.latest_snapshot_event AND supporter_s.initiative_id = initiative.id AND (supporter_s.member_id = ? OR supporter_s.member_id = delegating_interest_s.delegate_member_ids[array_upper(delegating_interest_s.delegate_member_ids, 1)])", options.member_id })
+ selector:left_join("direct_supporter_snapshot", "supporter_s", { "supporter_s.snapshot_id = issue.latest_snapshot_id AND supporter_s.initiative_id = initiative.id AND (supporter_s.member_id = ? OR supporter_s.member_id = delegating_interest_s.delegate_member_ids[array_upper(delegating_interest_s.delegate_member_ids, 1)])", options.member_id })
selector:add_field("CASE WHEN issue.fully_frozen ISNULL AND issue.closed ISNULL THEN supporter.member_id NOTNULL ELSE supporter_s.member_id NOTNULL END", "supported")
selector:add_field({ "CASE WHEN issue.fully_frozen ISNULL AND issue.closed ISNULL THEN delegation_info.own_participation AND supporter.member_id NOTNULL ELSE supporter_s.member_id = ? END", options.member_id }, "directly_supported")
diff -r 7ea154c9238a -r 32cc544d5a5b model/issue.lua
--- a/model/issue.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/issue.lua Sun Jul 15 14:07:29 2018 +0200
@@ -162,7 +162,7 @@
selector:add_field ( "non_voter.member_id NOTNULL", "non_voter" )
selector:left_join ( "direct_interest_snapshot", nil, { [[
direct_interest_snapshot.issue_id = issue.id AND
- direct_interest_snapshot.event = issue.latest_snapshot_event AND
+ direct_interest_snapshot.snapshot_id = issue.latest_snapshot_id AND
direct_interest_snapshot.member_id = ?
]], options.member_id })
selector:add_field ( "direct_interest_snapshot.weight", "weight" )
@@ -275,3 +275,10 @@
return _("ends in #{state_time_left}", { state_time_left = self.state_time_left })
end
end
+
+function Issue:by_ids(ids)
+ local selector = self:new_selector()
+ selector:add_where{'"id" IN ($)', { ids } }
+ return selector:exec()
+end
+
diff -r 7ea154c9238a -r 32cc544d5a5b model/member.lua
--- a/model/member.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/member.lua Sun Jul 15 14:07:29 2018 +0200
@@ -166,6 +166,24 @@
}
Member:add_reference{
+ mode = '11',
+ to = "MemberProfile",
+ this_key = 'id',
+ that_key = 'member_id',
+ ref = 'profile',
+ back_ref = 'member'
+}
+
+Member:add_reference{
+ mode = '11',
+ to = "MemberSettings",
+ this_key = 'id',
+ that_key = 'member_id',
+ ref = 'settings',
+ back_ref = 'member'
+}
+
+Member:add_reference{
mode = 'mm',
to = "Member",
this_key = 'id',
@@ -242,8 +260,6 @@
ref = 'supported_initiatives'
}
-model.has_rendered_content(Member, RenderedMemberStatement, "statement")
-
function Member:build_selector(args)
local selector = self:new_selector()
if args.active ~= nil then
@@ -531,6 +547,12 @@
end
+function Member:by_ids(ids)
+ local selector = self:new_selector()
+ selector:add_where{'"id" IN ($)', { ids } }
+ return selector:exec()
+end
+
function Member:by_login(login)
local selector = self:new_selector()
selector:add_where{'"login" = ?', login }
@@ -589,19 +611,23 @@
local subject = subject
local content
-
+ local baseurl = request.get_absolute_baseurl() .. "index/register.html"
+ local url = baseurl .. "?skip=1&code=" .. self.invite_code
if template_file then
local fh = io.open(template_file, "r")
content = fh:read("*a")
- content = (content:gsub("#{invite_code}", self.invite_code))
+ content = content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
+ elseif config.invitation_mail then
+ subject = config.invitation_mail.subject
+ content = config.invitation_mail.content:gsub("#{url}", url):gsub("#{baseurl}", baseurl):gsub("#{code}", self.invite_code)
else
subject = config.mail_subject_prefix .. _"Invitation to LiquidFeedback"
content = slot.use_temporary(function()
slot.put(_"Hello\n\n")
slot.put(_"You are invited to LiquidFeedback. To register please click the following link:\n\n")
- slot.put(request.get_absolute_baseurl() .. "index/register.html?invite=" .. self.invite_code .. "\n\n")
+ slot.put(url .. "\n\n")
slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
- slot.put(request.get_absolute_baseurl() .. "index/register.html\n\n")
+ slot.put(baseurl .. "\n\n")
slot.put(_"On that page please enter the invite key:\n\n")
slot.put(self.invite_code .. "\n\n")
end)
@@ -738,7 +764,33 @@
end
end
-function Member.object:has_voting_right_for_unit_id(unit_id)
+local function populate_units_with_initiative_right_hash(self)
+ if not self.__units_with_initiative_right_hash then
+ local privileges = Privilege:new_selector()
+ :add_where{ "member_id = ?", self.id }
+ :add_where("initiative_right")
+ :exec()
+ self.__units_with_initiative_right_hash = {}
+ for i, privilege in ipairs(privileges) do
+ self.__units_with_initiative_right_hash[privilege.unit_id] = true
+ end
+ end
+end
+
+function Member.object:has_initiative_right_for_unit_id(unit_id)
+ populate_units_with_initiative_right_hash(self)
+ return self.__units_with_initiative_right_hash[unit_id] and true or false
+end
+
+function Member.object_get:has_initiative_right()
+ populate_units_with_initiative_right_hash(self)
+ for k, v in pairs(self.__units_with_initiative_right_hash) do
+ return true
+ end
+ return false
+end
+
+local function populate_units_with_voting_right_hash(self)
if not self.__units_with_voting_right_hash then
local privileges = Privilege:new_selector()
:add_where{ "member_id = ?", self.id }
@@ -749,6 +801,18 @@
self.__units_with_voting_right_hash[privilege.unit_id] = true
end
end
+end
+
+function Member.object_get:has_voting_right()
+ populate_units_with_voting_right_hash(self)
+ for k, v in pairs(self.__units_with_voting_right_hash) do
+ return true
+ end
+ return false
+end
+
+function Member.object:has_voting_right_for_unit_id(unit_id)
+ populate_units_with_voting_right_hash(self)
return self.__units_with_voting_right_hash[unit_id] and true or false
end
@@ -778,3 +842,13 @@
function Member.object:delete()
db:query{ "SELECT delete_member(?)", self.id }
end
+
+function Member.object_get:display_name()
+ if self.identification then
+ return self.identification
+ elseif self.name then
+ return self.name
+ else
+ return "Member #" .. self.id
+ end
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/member_application.lua
--- a/model/member_application.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/member_application.lua Sun Jul 15 14:07:29 2018 +0200
@@ -1,2 +1,66 @@
MemberApplication = mondelefant.new_class()
MemberApplication.table = 'member_application'
+
+MemberApplication:add_reference{
+ mode = 'm1',
+ to = "SystemApplication",
+ this_key = 'system_application_id',
+ that_key = 'id',
+ ref = 'system_application'
+}
+
+function MemberApplication:get_selector_by_member_id_and_system_application_id(member_id, system_application_id)
+ local selector = self:new_selector()
+ selector:add_where{ "member_id = ?", member_id }
+ selector:add_where{ "system_application_id = ?", system_application_id }
+ selector:optional_object_mode()
+ return selector
+end
+
+function MemberApplication:by_member_id_and_system_application_id(member_id, system_application_id)
+ local member_application = self:get_selector_by_member_id_and_system_application_id(member_id, system_application_id)
+ :optional_object_mode()
+ :exec()
+ return member_application
+end
+
+function MemberApplication:get_selector_by_member_id_and_domain(member_id, domain)
+ local selector = self:new_selector()
+ selector:add_where{ "member_id = ?", member_id }
+ selector:add_where{ "domain = ?", domain }
+ selector:optional_object_mode()
+ return selector
+end
+
+function MemberApplication:by_member_id_and_domain(member_id, domain)
+ local member_application = self:get_selector_by_member_id_and_domain(member_id, domain)
+ :optional_object_mode()
+ :exec()
+ return member_application
+end
+
+function MemberApplication:by_member_id(member_id)
+ local member_applications = self:new_selector()
+ :add_where{ "member_id = ?", member_id }
+ :exec()
+ return member_applications
+end
+
+function MemberApplication:by_member_id_with_domain(member_id)
+ local member_applications = self:new_selector()
+ :add_where{ "member_id = ?", member_id }
+ :add_where( "domain NOTNULL" )
+ :exec()
+ return member_applications
+end
+
+function MemberApplication:by_member_id_and_origin(member_id, origin)
+ local domain = string.match(string.lower(origin), "^https://(.+)")
+ if not domain then
+ return
+ end
+ local member_application = self:get_selector_by_member_id_and_domain(member_id, domain)
+ :optional_object_mode()
+ :exec()
+ return member_application
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/member_profile.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/member_profile.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,6 @@
+MemberProfile = mondelefant.new_class()
+MemberProfile.table = 'member_profile'
+MemberProfile.primary_key = "member_id"
+
+model.has_rendered_content(MemberProfile, RenderedMemberStatement, "statement", "member_id")
+
diff -r 7ea154c9238a -r 32cc544d5a5b model/member_settings.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/member_settings.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,5 @@
+MemberSettings = mondelefant.new_class()
+MemberSettings.table = 'member_settings'
+MemberSettings.primary_key = "member_id"
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b model/member_useterms.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/member_useterms.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,5 @@
+MemberUseterms = mondelefant.new_class()
+MemberUseterms.table = 'member_useterms'
+MemberUseterms.primary_key = { "member_id", "contract_identifier" }
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b model/role_verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/role_verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,3 @@
+RoleVerification = mondelefant.new_class()
+RoleVerification.table = "role_verification"
+RoleVerification.primary_key = "id"
diff -r 7ea154c9238a -r 32cc544d5a5b model/session.lua
--- a/model/session.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/session.lua Sun Jul 15 14:07:29 2018 +0200
@@ -10,21 +10,57 @@
ref = 'member',
}
-local function random_string()
+Session:add_reference{
+ mode = 'm1',
+ to = "Member",
+ this_key = 'real_member_id',
+ that_key = 'id',
+ ref = 'real_member',
+}
+
+local secret_length = 24
+local secret_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+local secret_purposes = { "oauth", "csrf", "_other" }
+for idx, purpose in ipairs(secret_purposes) do
+ secret_purposes[purpose] = idx
+end
+
+local function random_string(length_multiplier)
return multirand.string(
- 32,
- '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ secret_length * (length_multiplier or 1),
+ secret_alphabet
)
end
function Session:new()
local session = self.prototype.new(self) -- super call
session.ident = random_string()
- session.additional_secret = random_string()
- session:save()
+ session.additional_secret = random_string(#secret_purposes)
+ session:save()
return session
end
+function Session.object:additional_secret_for(purpose)
+ local use_hash = false
+ local idx = secret_purposes[purpose]
+ if not idx then
+ idx = assert(secret_purposes._other, "No other secrets supported")
+ use_hash = true
+ end
+ local from_pos = secret_length * (idx-1) + 1
+ local to_pos = from_pos + secret_length - 1
+ local secret = string.sub(self.additional_secret, from_pos, to_pos)
+ if #secret ~= secret_length then
+ self:destroy()
+ error("Session state invalid")
+ end
+ if use_hash then
+ local moonhash = require "moonhash" -- TODO: auto loader for libraries in WebMCP?
+ secret = moonhash.shake256(secret .. "\0" .. purpose, secret_length, secret_alphabet)
+ end
+ return secret
+end
+
function Session:by_ident(ident)
local selector = self:new_selector()
selector:add_where{ 'ident = ?', ident }
diff -r 7ea154c9238a -r 32cc544d5a5b model/suggestion.lua
--- a/model/suggestion.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/suggestion.lua Sun Jul 15 14:07:29 2018 +0200
@@ -27,4 +27,4 @@
default_order = '"id"'
}
-model.has_rendered_content(Suggestion, RenderedSuggestion)
+model.has_rendered_content(Suggestion, RenderedSuggestion, "content", "suggestion_id")
diff -r 7ea154c9238a -r 32cc544d5a5b model/system_application.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/system_application.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,28 @@
+SystemApplication = mondelefant.new_class()
+SystemApplication.table = 'system_application'
+
+
+function SystemApplication:by_client_id(client_id)
+ local system_application = self:new_selector()
+ :add_where{ "client_id = ?", client_id }
+ :optional_object_mode()
+ :exec()
+ return system_application
+end
+
+function SystemApplication:by_origin(origin)
+ local system_applications = self:new_selector()
+ :set_distinct()
+ :left_join("system_application_redirect_uri", nil, "system_application_redirect_uri.system_application_id = system_application.id")
+ :add_where{ "lower(regexp_replace(system_application.default_redirect_uri, '^([^:]+://[^/]+)/.*', E'\\\\1', 'i')) = lower(?) OR lower(regexp_replace(system_application_redirect_uri.redirect_uri, '^([^:]+://[^/]+)/.*', E'\\\\1', 'i')) = lower(?)", origin, origin }
+ :exec()
+ return system_applications
+end
+
+function SystemApplication:get_all()
+ local system_application = self:new_selector()
+ :exec()
+ return system_application
+end
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b model/system_application_redirect_uri.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/system_application_redirect_uri.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,12 @@
+SystemApplicationRedirectUri = mondelefant.new_class()
+SystemApplicationRedirectUri.table = 'system_application_redirect_uri'
+
+
+function SystemApplicationRedirectUri:by_pk(system_application_id, redirect_uri)
+ local system_application_redirect_uri = self:new_selector()
+ :add_where{ "system_application_id = ?", system_application_id }
+ :add_where{ "redirect_uri = ?", redirect_uri }
+ :optional_object_mode()
+ :exec()
+ return system_application_redirect_uri
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/token.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/token.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,147 @@
+Token = mondelefant.new_class()
+Token.table = 'token'
+
+Token:add_reference{
+ mode = '1m',
+ to = "TokenScope",
+ this_key = 'id',
+ that_key = 'token_id',
+ ref = 'token_scopes',
+ back_ref = 'token',
+ default_order = 'token_scope.index'
+}
+
+Token:add_reference{
+ mode = 'm1',
+ to = "Member",
+ this_key = 'member_id',
+ that_key = 'id',
+ ref = 'member',
+}
+
+Token:add_reference{
+ mode = 'm1',
+ to = "Session",
+ this_key = 'session_id',
+ that_key = 'id',
+ ref = 'session',
+}
+
+Token:add_reference{
+ mode = 'm1',
+ to = "SystemApplication",
+ this_key = 'system_application_id',
+ that_key = 'id',
+ ref = 'system_application',
+}
+
+function Token:new()
+ local token = self.prototype.new(self)
+ token.token = multirand.string(16, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+ return token
+end
+
+function Token:create_authorization(member_id, system_application_id, domain, session_id, redirect_uri, redirect_uri_explicit, scopes, state)
+
+ local detached = false
+ for i = 0, #scopes do
+ if scopes[i] then
+ for s in string.gmatch(scopes[i], "[^ ]+") do
+ if s == "detached" then
+ detached = true
+ end
+ end
+ end
+ end
+
+ local requested_scopes = {}
+
+ for i = 0, #scopes do
+ if scopes[i] then
+ for scope in string.gmatch(scopes[i], "[^ ]+") do
+ requested_scopes[scope] = true
+ end
+ end
+ end
+
+ local requested_scopes_list = {}
+
+ for k, v in pairs(requested_scopes) do
+ requested_scopes_list[#requested_scopes_list+1] = k
+ end
+
+ local requested_scopes_string = table.concat(requested_scopes_list, " ")
+
+ local expiry = db:query({"SELECT now() + (? || 'sec')::interval AS expiry", config.oauth2.authorization_code_lifetime }, "object").expiry
+
+ local token = Token:new()
+ token.token_type = "authorization"
+ token.member_id = member_id
+ token.system_application_id = system_application_id
+ token.domain = domain
+ if not detached then
+ token.session_id = session_id
+ end
+ token.redirect_uri = redirect_uri
+ token.redirect_uri_explicit = redirect_uri_explicit
+ token.expiry = expiry
+ token.scope = requested_scopes_string
+
+ token:save()
+
+ for i = 0, #scopes do
+ if scopes[i] then
+ local token_scope = TokenScope:new()
+ token_scope.token_id = token.id
+ token_scope.index = i
+ token_scope.scope = scopes[i]
+ token_scope:save()
+ end
+ end
+
+
+ return token, target_uri
+end
+
+function Token:by_token_type_and_token(token_type, token)
+ local selector = Token:new_selector()
+ selector:add_where{ "token_type = ?", token_type }
+ selector:add_where{ "token = ?", token }
+ selector:add_where{ "expiry > now()" }
+ selector:optional_object_mode()
+ if token_type == "authorization_code" then
+ selector:for_update()
+ end
+ if token_type == "access_token" then
+ selector:add_field("FLOOR(EXTRACT(EPOCH FROM expiry - now()))", "expiry_in")
+ end
+ return selector:exec()
+end
+
+function Token:refresh_token_by_token_selector(token)
+ local selector = Token:new_selector()
+ selector:add_where{ "token_type = ?", "refresh" }
+ selector:add_where{ "member_id = ?", token.member_id }
+ if token.system_application_id then
+ selector:add_where{ "system_application_id = ?", token.system_application_id }
+ else
+ selector:add_where{ "domain = ?", token.domain }
+ end
+ return selector
+end
+
+function Token:fresh_refresh_token_by_token(token)
+ local selector = Token:refresh_token_by_token_selector(token)
+ selector:add_where{ "created + ('?' || ' sec')::interval > now()", config.oauth2.refresh_pause }
+ selector:add_where{ "regexp_split_to_array(scope, E'\\\\s+') <@ regexp_split_to_array(?, E'\\\\s+')", token.scope }
+ selector:add_where{ "regexp_split_to_array(scope, E'\\\\s+') @> regexp_split_to_array(?, E'\\\\s+')", token.scope }
+ return selector:exec()
+end
+
+function Token:old_refresh_token_by_token(token, scopes)
+ local selector = Token:refresh_token_by_token_selector(token)
+ selector:add_where{ "id < ?", token.id }
+ selector:add_where{ "created + ('?' || ' sec')::interval <= now()", config.oauth2.refresh_grace_period }
+ selector:add_where{ "regexp_split_to_array(scope, E'\\\\s+') && regexp_split_to_array(?, E'\\\\s+')", scopes }
+ return selector:exec()
+end
diff -r 7ea154c9238a -r 32cc544d5a5b model/token_scope.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/token_scope.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,3 @@
+TokenScope = mondelefant.new_class()
+TokenScope.table = 'token_scope'
+TokenScope.primary_key = { "token_id", "index" }
diff -r 7ea154c9238a -r 32cc544d5a5b model/unit.lua
--- a/model/unit.lua Thu Jun 23 03:30:57 2016 +0200
+++ b/model/unit.lua Sun Jul 15 14:07:29 2018 +0200
@@ -7,7 +7,8 @@
this_key = 'id',
that_key = 'unit_id',
ref = 'areas',
- back_ref = 'unit'
+ back_ref = 'unit',
+ default_order = "area.name, area.id"
}
Unit:add_reference{
diff -r 7ea154c9238a -r 32cc544d5a5b model/verification.lua
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/model/verification.lua Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,3 @@
+Verification = mondelefant.new_class()
+Verification.table = "verification"
+Verification.primary_key = "id"
diff -r 7ea154c9238a -r 32cc544d5a5b static/font/Apache_License.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/font/Apache_License.txt Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,203 @@
+Font data copyright Google 2013
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff -r 7ea154c9238a -r 32cc544d5a5b static/js/dragdrop.js
--- a/static/js/dragdrop.js Thu Jun 23 03:30:57 2016 +0200
+++ b/static/js/dragdrop.js Sun Jul 15 14:07:29 2018 +0200
@@ -24,9 +24,10 @@
draggedElement.style.backgroundColor = "#eee";
draggedElement.style.opacity = 0.8;
originalElement.offsetParent.appendChild(draggedElement);
- // workaround for wrong clientWidth and clientHeight information:
- draggedElement.style.width = 2*originalElement.clientWidth - draggedElement.clientWidth;
- draggedElement.style.height = 2*originalElement.clientHeight - draggedElement.clientHeight;
+ draggedElement.style.width = originalElement.clientWidth + "px";
+ draggedElement.style.height = originalElement.clientHeight + "px";
+ draggedElement.style.left = originalElement.offsetLeft + "px";
+ draggedElement.style.top = originalElement.offsetTop + "px";
mouseOffsetX = mouseX;
mouseOffsetY = mouseY;
dropFunc = func;
@@ -36,8 +37,8 @@
mouseX = event.pageX;
mouseY = event.pageY;
if (draggedElement) {
- draggedElement.style.left = elementOffsetX + mouseX - mouseOffsetX;
- draggedElement.style.top = elementOffsetY + mouseY - mouseOffsetY;
+ draggedElement.style.left = elementOffsetX + mouseX - mouseOffsetX + "px";
+ draggedElement.style.top = elementOffsetY + mouseY - mouseOffsetY + "px";
}
});
}, true);
@@ -72,7 +73,7 @@
event.preventDefault();
});
}, false);
- } else if (element.className == "clickable") {
+ } else if (element.classList.contains("clickable")) {
element.addEventListener("mousedown", function(event) {
jsProtect(function() {
event.stopPropagation();
diff -r 7ea154c9238a -r 32cc544d5a5b static/mdl/LICENSE
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/mdl/LICENSE Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,212 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2015 Google Inc
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ All code in any directories or sub-directories that end with *.html or
+ *.css is licensed under the Creative Commons Attribution International
+ 4.0 License, which full text can be found here:
+ https://creativecommons.org/licenses/by/4.0/legalcode.
+
+ As an exception to this license, all html or css that is generated by
+ the software at the direction of the user is copyright the user. The
+ user has full ownership and control over such content, including
+ whether and how they wish to license it.
diff -r 7ea154c9238a -r 32cc544d5a5b static/mdl/buffer.svg
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/mdl/buffer.svg Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,9 @@
+
+
diff -r 7ea154c9238a -r 32cc544d5a5b static/mdl/material.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/static/mdl/material.js Sun Jul 15 14:07:29 2018 +0200
@@ -0,0 +1,3981 @@
+;(function() {
+"use strict";
+
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A component handler interface using the revealing module design pattern.
+ * More details on this design pattern here:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @author Jason Mayes.
+ */
+/* exported componentHandler */
+
+// Pre-defining the componentHandler interface, for closure documentation and
+// static verification.
+var componentHandler = {
+ /**
+ * Searches existing DOM for elements of our component type and upgrades them
+ * if they have not already been upgraded.
+ *
+ * @param {string=} optJsClass the programatic name of the element class we
+ * need to create a new instance of.
+ * @param {string=} optCssClass the name of the CSS class elements of this
+ * type will have.
+ */
+ upgradeDom: function(optJsClass, optCssClass) {},
+ /**
+ * Upgrades a specific element rather than all in the DOM.
+ *
+ * @param {!Element} element The element we wish to upgrade.
+ * @param {string=} optJsClass Optional name of the class we want to upgrade
+ * the element to.
+ */
+ upgradeElement: function(element, optJsClass) {},
+ /**
+ * Upgrades a specific list of elements rather than all in the DOM.
+ *
+ * @param {!Element|!Array|!NodeList|!HTMLCollection} elements
+ * The elements we wish to upgrade.
+ */
+ upgradeElements: function(elements) {},
+ /**
+ * Upgrades all registered components found in the current DOM. This is
+ * automatically called on window load.
+ */
+ upgradeAllRegistered: function() {},
+ /**
+ * Allows user to be alerted to any upgrades that are performed for a given
+ * component type
+ *
+ * @param {string} jsClass The class name of the MDL component we wish
+ * to hook into for any upgrades performed.
+ * @param {function(!HTMLElement)} callback The function to call upon an
+ * upgrade. This function should expect 1 parameter - the HTMLElement which
+ * got upgraded.
+ */
+ registerUpgradedCallback: function(jsClass, callback) {},
+ /**
+ * Registers a class for future use and attempts to upgrade existing DOM.
+ *
+ * @param {componentHandler.ComponentConfigPublic} config the registration configuration
+ */
+ register: function(config) {},
+ /**
+ * Downgrade either a given node, an array of nodes, or a NodeList.
+ *
+ * @param {!Node|!Array|!NodeList} nodes
+ */
+ downgradeElements: function(nodes) {}
+};
+
+componentHandler = (function() {
+ 'use strict';
+
+ /** @type {!Array} */
+ var registeredComponents_ = [];
+
+ /** @type {!Array} */
+ var createdComponents_ = [];
+
+ var componentConfigProperty_ = 'mdlComponentConfigInternal_';
+
+ /**
+ * Searches registered components for a class we are interested in using.
+ * Optionally replaces a match with passed object if specified.
+ *
+ * @param {string} name The name of a class we want to use.
+ * @param {componentHandler.ComponentConfig=} optReplace Optional object to replace match with.
+ * @return {!Object|boolean}
+ * @private
+ */
+ function findRegisteredClass_(name, optReplace) {
+ for (var i = 0; i < registeredComponents_.length; i++) {
+ if (registeredComponents_[i].className === name) {
+ if (typeof optReplace !== 'undefined') {
+ registeredComponents_[i] = optReplace;
+ }
+ return registeredComponents_[i];
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns an array of the classNames of the upgraded classes on the element.
+ *
+ * @param {!Element} element The element to fetch data from.
+ * @return {!Array}
+ * @private
+ */
+ function getUpgradedListOfElement_(element) {
+ var dataUpgraded = element.getAttribute('data-upgraded');
+ // Use `['']` as default value to conform the `,name,name...` style.
+ return dataUpgraded === null ? [''] : dataUpgraded.split(',');
+ }
+
+ /**
+ * Returns true if the given element has already been upgraded for the given
+ * class.
+ *
+ * @param {!Element} element The element we want to check.
+ * @param {string} jsClass The class to check for.
+ * @returns {boolean}
+ * @private
+ */
+ function isElementUpgraded_(element, jsClass) {
+ var upgradedList = getUpgradedListOfElement_(element);
+ return upgradedList.indexOf(jsClass) !== -1;
+ }
+
+ /**
+ * Searches existing DOM for elements of our component type and upgrades them
+ * if they have not already been upgraded.
+ *
+ * @param {string=} optJsClass the programatic name of the element class we
+ * need to create a new instance of.
+ * @param {string=} optCssClass the name of the CSS class elements of this
+ * type will have.
+ */
+ function upgradeDomInternal(optJsClass, optCssClass) {
+ if (typeof optJsClass === 'undefined' &&
+ typeof optCssClass === 'undefined') {
+ for (var i = 0; i < registeredComponents_.length; i++) {
+ upgradeDomInternal(registeredComponents_[i].className,
+ registeredComponents_[i].cssClass);
+ }
+ } else {
+ var jsClass = /** @type {string} */ (optJsClass);
+ if (typeof optCssClass === 'undefined') {
+ var registeredClass = findRegisteredClass_(jsClass);
+ if (registeredClass) {
+ optCssClass = registeredClass.cssClass;
+ }
+ }
+
+ var elements = document.querySelectorAll('.' + optCssClass);
+ for (var n = 0; n < elements.length; n++) {
+ upgradeElementInternal(elements[n], jsClass);
+ }
+ }
+ }
+
+ /**
+ * Upgrades a specific element rather than all in the DOM.
+ *
+ * @param {!Element} element The element we wish to upgrade.
+ * @param {string=} optJsClass Optional name of the class we want to upgrade
+ * the element to.
+ */
+ function upgradeElementInternal(element, optJsClass) {
+ // Verify argument type.
+ if (!(typeof element === 'object' && element instanceof Element)) {
+ throw new Error('Invalid argument provided to upgrade MDL element.');
+ }
+ var upgradedList = getUpgradedListOfElement_(element);
+ var classesToUpgrade = [];
+ // If jsClass is not provided scan the registered components to find the
+ // ones matching the element's CSS classList.
+ if (!optJsClass) {
+ var classList = element.classList;
+ registeredComponents_.forEach(function(component) {
+ // Match CSS & Not to be upgraded & Not upgraded.
+ if (classList.contains(component.cssClass) &&
+ classesToUpgrade.indexOf(component) === -1 &&
+ !isElementUpgraded_(element, component.className)) {
+ classesToUpgrade.push(component);
+ }
+ });
+ } else if (!isElementUpgraded_(element, optJsClass)) {
+ classesToUpgrade.push(findRegisteredClass_(optJsClass));
+ }
+
+ // Upgrade the element for each classes.
+ for (var i = 0, n = classesToUpgrade.length, registeredClass; i < n; i++) {
+ registeredClass = classesToUpgrade[i];
+ if (registeredClass) {
+ // Mark element as upgraded.
+ upgradedList.push(registeredClass.className);
+ element.setAttribute('data-upgraded', upgradedList.join(','));
+ var instance = new registeredClass.classConstructor(element);
+ instance[componentConfigProperty_] = registeredClass;
+ createdComponents_.push(instance);
+ // Call any callbacks the user has registered with this component type.
+ for (var j = 0, m = registeredClass.callbacks.length; j < m; j++) {
+ registeredClass.callbacks[j](element);
+ }
+
+ if (registeredClass.widget) {
+ // Assign per element instance for control over API
+ element[registeredClass.className] = instance;
+ }
+ } else {
+ throw new Error(
+ 'Unable to find a registered component for the given class.');
+ }
+
+ var ev;
+ if ('CustomEvent' in window && typeof window.CustomEvent === 'function') {
+ ev = new CustomEvent('mdl-componentupgraded', {
+ bubbles: true, cancelable: false
+ });
+ } else {
+ ev = document.createEvent('Events');
+ ev.initEvent('mdl-componentupgraded', true, true);
+ }
+ element.dispatchEvent(ev);
+ }
+ }
+
+ /**
+ * Upgrades a specific list of elements rather than all in the DOM.
+ *
+ * @param {!Element|!Array|!NodeList|!HTMLCollection} elements
+ * The elements we wish to upgrade.
+ */
+ function upgradeElementsInternal(elements) {
+ if (!Array.isArray(elements)) {
+ if (elements instanceof Element) {
+ elements = [elements];
+ } else {
+ elements = Array.prototype.slice.call(elements);
+ }
+ }
+ for (var i = 0, n = elements.length, element; i < n; i++) {
+ element = elements[i];
+ if (element instanceof HTMLElement) {
+ upgradeElementInternal(element);
+ if (element.children.length > 0) {
+ upgradeElementsInternal(element.children);
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers a class for future use and attempts to upgrade existing DOM.
+ *
+ * @param {componentHandler.ComponentConfigPublic} config
+ */
+ function registerInternal(config) {
+ // In order to support both Closure-compiled and uncompiled code accessing
+ // this method, we need to allow for both the dot and array syntax for
+ // property access. You'll therefore see the `foo.bar || foo['bar']`
+ // pattern repeated across this method.
+ var widgetMissing = (typeof config.widget === 'undefined' &&
+ typeof config['widget'] === 'undefined');
+ var widget = true;
+
+ if (!widgetMissing) {
+ widget = config.widget || config['widget'];
+ }
+
+ var newConfig = /** @type {componentHandler.ComponentConfig} */ ({
+ classConstructor: config.constructor || config['constructor'],
+ className: config.classAsString || config['classAsString'],
+ cssClass: config.cssClass || config['cssClass'],
+ widget: widget,
+ callbacks: []
+ });
+
+ registeredComponents_.forEach(function(item) {
+ if (item.cssClass === newConfig.cssClass) {
+ throw new Error('The provided cssClass has already been registered: ' + item.cssClass);
+ }
+ if (item.className === newConfig.className) {
+ throw new Error('The provided className has already been registered');
+ }
+ });
+
+ if (config.constructor.prototype
+ .hasOwnProperty(componentConfigProperty_)) {
+ throw new Error(
+ 'MDL component classes must not have ' + componentConfigProperty_ +
+ ' defined as a property.');
+ }
+
+ var found = findRegisteredClass_(config.classAsString, newConfig);
+
+ if (!found) {
+ registeredComponents_.push(newConfig);
+ }
+ }
+
+ /**
+ * Allows user to be alerted to any upgrades that are performed for a given
+ * component type
+ *
+ * @param {string} jsClass The class name of the MDL component we wish
+ * to hook into for any upgrades performed.
+ * @param {function(!HTMLElement)} callback The function to call upon an
+ * upgrade. This function should expect 1 parameter - the HTMLElement which
+ * got upgraded.
+ */
+ function registerUpgradedCallbackInternal(jsClass, callback) {
+ var regClass = findRegisteredClass_(jsClass);
+ if (regClass) {
+ regClass.callbacks.push(callback);
+ }
+ }
+
+ /**
+ * Upgrades all registered components found in the current DOM. This is
+ * automatically called on window load.
+ */
+ function upgradeAllRegisteredInternal() {
+ for (var n = 0; n < registeredComponents_.length; n++) {
+ upgradeDomInternal(registeredComponents_[n].className);
+ }
+ }
+
+ /**
+ * Check the component for the downgrade method.
+ * Execute if found.
+ * Remove component from createdComponents list.
+ *
+ * @param {?componentHandler.Component} component
+ */
+ function deconstructComponentInternal(component) {
+ if (component) {
+ var componentIndex = createdComponents_.indexOf(component);
+ createdComponents_.splice(componentIndex, 1);
+
+ var upgrades = component.element_.getAttribute('data-upgraded').split(',');
+ var componentPlace = upgrades.indexOf(component[componentConfigProperty_].classAsString);
+ upgrades.splice(componentPlace, 1);
+ component.element_.setAttribute('data-upgraded', upgrades.join(','));
+
+ var ev;
+ if ('CustomEvent' in window && typeof window.CustomEvent === 'function') {
+ ev = new CustomEvent('mdl-componentdowngraded', {
+ bubbles: true, cancelable: false
+ });
+ } else {
+ ev = document.createEvent('Events');
+ ev.initEvent('mdl-componentdowngraded', true, true);
+ }
+ component.element_.dispatchEvent(ev);
+ }
+ }
+
+ /**
+ * Downgrade either a given node, an array of nodes, or a NodeList.
+ *
+ * @param {!Node|!Array|!NodeList} nodes
+ */
+ function downgradeNodesInternal(nodes) {
+ /**
+ * Auxiliary function to downgrade a single node.
+ * @param {!Node} node the node to be downgraded
+ */
+ var downgradeNode = function(node) {
+ createdComponents_.filter(function(item) {
+ return item.element_ === node;
+ }).forEach(deconstructComponentInternal);
+ };
+ if (nodes instanceof Array || nodes instanceof NodeList) {
+ for (var n = 0; n < nodes.length; n++) {
+ downgradeNode(nodes[n]);
+ }
+ } else if (nodes instanceof Node) {
+ downgradeNode(nodes);
+ } else {
+ throw new Error('Invalid argument provided to downgrade MDL nodes.');
+ }
+ }
+
+ // Now return the functions that should be made public with their publicly
+ // facing names...
+ return {
+ upgradeDom: upgradeDomInternal,
+ upgradeElement: upgradeElementInternal,
+ upgradeElements: upgradeElementsInternal,
+ upgradeAllRegistered: upgradeAllRegisteredInternal,
+ registerUpgradedCallback: registerUpgradedCallbackInternal,
+ register: registerInternal,
+ downgradeElements: downgradeNodesInternal
+ };
+})();
+
+/**
+ * Describes the type of a registered component type managed by
+ * componentHandler. Provided for benefit of the Closure compiler.
+ *
+ * @typedef {{
+ * constructor: Function,
+ * classAsString: string,
+ * cssClass: string,
+ * widget: (string|boolean|undefined)
+ * }}
+ */
+componentHandler.ComponentConfigPublic; // jshint ignore:line
+
+/**
+ * Describes the type of a registered component type managed by
+ * componentHandler. Provided for benefit of the Closure compiler.
+ *
+ * @typedef {{
+ * constructor: !Function,
+ * className: string,
+ * cssClass: string,
+ * widget: (string|boolean),
+ * callbacks: !Array
+ * }}
+ */
+componentHandler.ComponentConfig; // jshint ignore:line
+
+/**
+ * Created component (i.e., upgraded element) type as managed by
+ * componentHandler. Provided for benefit of the Closure compiler.
+ *
+ * @typedef {{
+ * element_: !HTMLElement,
+ * className: string,
+ * classAsString: string,
+ * cssClass: string,
+ * widget: string
+ * }}
+ */
+componentHandler.Component; // jshint ignore:line
+
+// Export all symbols, for the benefit of Closure compiler.
+// No effect on uncompiled code.
+componentHandler['upgradeDom'] = componentHandler.upgradeDom;
+componentHandler['upgradeElement'] = componentHandler.upgradeElement;
+componentHandler['upgradeElements'] = componentHandler.upgradeElements;
+componentHandler['upgradeAllRegistered'] =
+ componentHandler.upgradeAllRegistered;
+componentHandler['registerUpgradedCallback'] =
+ componentHandler.registerUpgradedCallback;
+componentHandler['register'] = componentHandler.register;
+componentHandler['downgradeElements'] = componentHandler.downgradeElements;
+window.componentHandler = componentHandler;
+window['componentHandler'] = componentHandler;
+
+window.addEventListener('load', function() {
+ 'use strict';
+
+ /**
+ * Performs a "Cutting the mustard" test. If the browser supports the features
+ * tested, adds a mdl-js class to the element. It then upgrades all MDL
+ * components requiring JavaScript.
+ */
+ if ('classList' in document.createElement('div') &&
+ 'querySelector' in document &&
+ 'addEventListener' in window && Array.prototype.forEach) {
+ document.documentElement.classList.add('mdl-js');
+ componentHandler.upgradeAllRegistered();
+ } else {
+ /**
+ * Dummy function to avoid JS errors.
+ */
+ componentHandler.upgradeElement = function() {};
+ /**
+ * Dummy function to avoid JS errors.
+ */
+ componentHandler.register = function() {};
+ }
+});
+
+// Source: https://github.com/darius/requestAnimationFrame/blob/master/requestAnimationFrame.js
+// Adapted from https://gist.github.com/paulirish/1579671 which derived from
+// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
+// requestAnimationFrame polyfill by Erik Möller.
+// Fixes from Paul Irish, Tino Zijdel, Andrew Mao, Klemen Slavič, Darius Bacon
+// MIT license
+if (!Date.now) {
+ /**
+ * Date.now polyfill.
+ * @return {number} the current Date
+ */
+ Date.now = function () {
+ return new Date().getTime();
+ };
+ Date['now'] = Date.now;
+}
+var vendors = [
+ 'webkit',
+ 'moz'
+];
+for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
+ var vp = vendors[i];
+ window.requestAnimationFrame = window[vp + 'RequestAnimationFrame'];
+ window.cancelAnimationFrame = window[vp + 'CancelAnimationFrame'] || window[vp + 'CancelRequestAnimationFrame'];
+ window['requestAnimationFrame'] = window.requestAnimationFrame;
+ window['cancelAnimationFrame'] = window.cancelAnimationFrame;
+}
+if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) || !window.requestAnimationFrame || !window.cancelAnimationFrame) {
+ var lastTime = 0;
+ /**
+ * requestAnimationFrame polyfill.
+ * @param {!Function} callback the callback function.
+ */
+ window.requestAnimationFrame = function (callback) {
+ var now = Date.now();
+ var nextTime = Math.max(lastTime + 16, now);
+ return setTimeout(function () {
+ callback(lastTime = nextTime);
+ }, nextTime - now);
+ };
+ window.cancelAnimationFrame = clearTimeout;
+ window['requestAnimationFrame'] = window.requestAnimationFrame;
+ window['cancelAnimationFrame'] = window.cancelAnimationFrame;
+}
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Button MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialButton = function MaterialButton(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialButton'] = MaterialButton;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialButton.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialButton.prototype.CssClasses_ = {
+ RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_CONTAINER: 'mdl-button__ripple-container',
+ RIPPLE: 'mdl-ripple'
+};
+/**
+ * Handle blur of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialButton.prototype.blurHandler_ = function (event) {
+ if (event) {
+ this.element_.blur();
+ }
+};
+// Public methods.
+/**
+ * Disable button.
+ *
+ * @public
+ */
+MaterialButton.prototype.disable = function () {
+ this.element_.disabled = true;
+};
+MaterialButton.prototype['disable'] = MaterialButton.prototype.disable;
+/**
+ * Enable button.
+ *
+ * @public
+ */
+MaterialButton.prototype.enable = function () {
+ this.element_.disabled = false;
+};
+MaterialButton.prototype['enable'] = MaterialButton.prototype.enable;
+/**
+ * Initialize element.
+ */
+MaterialButton.prototype.init = function () {
+ if (this.element_) {
+ if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
+ var rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER);
+ this.rippleElement_ = document.createElement('span');
+ this.rippleElement_.classList.add(this.CssClasses_.RIPPLE);
+ rippleContainer.appendChild(this.rippleElement_);
+ this.boundRippleBlurHandler = this.blurHandler_.bind(this);
+ this.rippleElement_.addEventListener('mouseup', this.boundRippleBlurHandler);
+ this.element_.appendChild(rippleContainer);
+ }
+ this.boundButtonBlurHandler = this.blurHandler_.bind(this);
+ this.element_.addEventListener('mouseup', this.boundButtonBlurHandler);
+ this.element_.addEventListener('mouseleave', this.boundButtonBlurHandler);
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialButton,
+ classAsString: 'MaterialButton',
+ cssClass: 'mdl-js-button',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Checkbox MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialCheckbox = function MaterialCheckbox(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialCheckbox'] = MaterialCheckbox;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialCheckbox.prototype.Constant_ = { TINY_TIMEOUT: 0.001 };
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialCheckbox.prototype.CssClasses_ = {
+ INPUT: 'mdl-checkbox__input',
+ BOX_OUTLINE: 'mdl-checkbox__box-outline',
+ FOCUS_HELPER: 'mdl-checkbox__focus-helper',
+ TICK_OUTLINE: 'mdl-checkbox__tick-outline',
+ RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ RIPPLE_CONTAINER: 'mdl-checkbox__ripple-container',
+ RIPPLE_CENTER: 'mdl-ripple--center',
+ RIPPLE: 'mdl-ripple',
+ IS_FOCUSED: 'is-focused',
+ IS_DISABLED: 'is-disabled',
+ IS_CHECKED: 'is-checked',
+ IS_UPGRADED: 'is-upgraded'
+};
+/**
+ * Handle change of state.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialCheckbox.prototype.onChange_ = function (event) {
+ this.updateClasses_();
+};
+/**
+ * Handle focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialCheckbox.prototype.onFocus_ = function (event) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle lost focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialCheckbox.prototype.onBlur_ = function (event) {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle mouseup.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialCheckbox.prototype.onMouseUp_ = function (event) {
+ this.blur_();
+};
+/**
+ * Handle class updates.
+ *
+ * @private
+ */
+MaterialCheckbox.prototype.updateClasses_ = function () {
+ this.checkDisabled();
+ this.checkToggleState();
+};
+/**
+ * Add blur.
+ *
+ * @private
+ */
+MaterialCheckbox.prototype.blur_ = function () {
+ // TODO: figure out why there's a focus event being fired after our blur,
+ // so that we can avoid this hack.
+ window.setTimeout(function () {
+ this.inputElement_.blur();
+ }.bind(this), this.Constant_.TINY_TIMEOUT);
+};
+// Public methods.
+/**
+ * Check the inputs toggle state and update display.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.checkToggleState = function () {
+ if (this.inputElement_.checked) {
+ this.element_.classList.add(this.CssClasses_.IS_CHECKED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_CHECKED);
+ }
+};
+MaterialCheckbox.prototype['checkToggleState'] = MaterialCheckbox.prototype.checkToggleState;
+/**
+ * Check the inputs disabled state and update display.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.checkDisabled = function () {
+ if (this.inputElement_.disabled) {
+ this.element_.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+};
+MaterialCheckbox.prototype['checkDisabled'] = MaterialCheckbox.prototype.checkDisabled;
+/**
+ * Disable checkbox.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.disable = function () {
+ this.inputElement_.disabled = true;
+ this.updateClasses_();
+};
+MaterialCheckbox.prototype['disable'] = MaterialCheckbox.prototype.disable;
+/**
+ * Enable checkbox.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.enable = function () {
+ this.inputElement_.disabled = false;
+ this.updateClasses_();
+};
+MaterialCheckbox.prototype['enable'] = MaterialCheckbox.prototype.enable;
+/**
+ * Check checkbox.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.check = function () {
+ this.inputElement_.checked = true;
+ this.updateClasses_();
+};
+MaterialCheckbox.prototype['check'] = MaterialCheckbox.prototype.check;
+/**
+ * Uncheck checkbox.
+ *
+ * @public
+ */
+MaterialCheckbox.prototype.uncheck = function () {
+ this.inputElement_.checked = false;
+ this.updateClasses_();
+};
+MaterialCheckbox.prototype['uncheck'] = MaterialCheckbox.prototype.uncheck;
+/**
+ * Initialize element.
+ */
+MaterialCheckbox.prototype.init = function () {
+ if (this.element_) {
+ this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT);
+ var boxOutline = document.createElement('span');
+ boxOutline.classList.add(this.CssClasses_.BOX_OUTLINE);
+ var tickContainer = document.createElement('span');
+ tickContainer.classList.add(this.CssClasses_.FOCUS_HELPER);
+ var tickOutline = document.createElement('span');
+ tickOutline.classList.add(this.CssClasses_.TICK_OUTLINE);
+ boxOutline.appendChild(tickOutline);
+ this.element_.appendChild(tickContainer);
+ this.element_.appendChild(boxOutline);
+ if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ this.rippleContainerElement_ = document.createElement('span');
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CONTAINER);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_EFFECT);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER);
+ this.boundRippleMouseUp = this.onMouseUp_.bind(this);
+ this.rippleContainerElement_.addEventListener('mouseup', this.boundRippleMouseUp);
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.RIPPLE);
+ this.rippleContainerElement_.appendChild(ripple);
+ this.element_.appendChild(this.rippleContainerElement_);
+ }
+ this.boundInputOnChange = this.onChange_.bind(this);
+ this.boundInputOnFocus = this.onFocus_.bind(this);
+ this.boundInputOnBlur = this.onBlur_.bind(this);
+ this.boundElementMouseUp = this.onMouseUp_.bind(this);
+ this.inputElement_.addEventListener('change', this.boundInputOnChange);
+ this.inputElement_.addEventListener('focus', this.boundInputOnFocus);
+ this.inputElement_.addEventListener('blur', this.boundInputOnBlur);
+ this.element_.addEventListener('mouseup', this.boundElementMouseUp);
+ this.updateClasses_();
+ this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialCheckbox,
+ classAsString: 'MaterialCheckbox',
+ cssClass: 'mdl-js-checkbox',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for icon toggle MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialIconToggle = function MaterialIconToggle(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialIconToggle'] = MaterialIconToggle;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialIconToggle.prototype.Constant_ = { TINY_TIMEOUT: 0.001 };
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialIconToggle.prototype.CssClasses_ = {
+ INPUT: 'mdl-icon-toggle__input',
+ JS_RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ RIPPLE_CONTAINER: 'mdl-icon-toggle__ripple-container',
+ RIPPLE_CENTER: 'mdl-ripple--center',
+ RIPPLE: 'mdl-ripple',
+ IS_FOCUSED: 'is-focused',
+ IS_DISABLED: 'is-disabled',
+ IS_CHECKED: 'is-checked'
+};
+/**
+ * Handle change of state.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialIconToggle.prototype.onChange_ = function (event) {
+ this.updateClasses_();
+};
+/**
+ * Handle focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialIconToggle.prototype.onFocus_ = function (event) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle lost focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialIconToggle.prototype.onBlur_ = function (event) {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle mouseup.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialIconToggle.prototype.onMouseUp_ = function (event) {
+ this.blur_();
+};
+/**
+ * Handle class updates.
+ *
+ * @private
+ */
+MaterialIconToggle.prototype.updateClasses_ = function () {
+ this.checkDisabled();
+ this.checkToggleState();
+};
+/**
+ * Add blur.
+ *
+ * @private
+ */
+MaterialIconToggle.prototype.blur_ = function () {
+ // TODO: figure out why there's a focus event being fired after our blur,
+ // so that we can avoid this hack.
+ window.setTimeout(function () {
+ this.inputElement_.blur();
+ }.bind(this), this.Constant_.TINY_TIMEOUT);
+};
+// Public methods.
+/**
+ * Check the inputs toggle state and update display.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.checkToggleState = function () {
+ if (this.inputElement_.checked) {
+ this.element_.classList.add(this.CssClasses_.IS_CHECKED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_CHECKED);
+ }
+};
+MaterialIconToggle.prototype['checkToggleState'] = MaterialIconToggle.prototype.checkToggleState;
+/**
+ * Check the inputs disabled state and update display.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.checkDisabled = function () {
+ if (this.inputElement_.disabled) {
+ this.element_.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+};
+MaterialIconToggle.prototype['checkDisabled'] = MaterialIconToggle.prototype.checkDisabled;
+/**
+ * Disable icon toggle.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.disable = function () {
+ this.inputElement_.disabled = true;
+ this.updateClasses_();
+};
+MaterialIconToggle.prototype['disable'] = MaterialIconToggle.prototype.disable;
+/**
+ * Enable icon toggle.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.enable = function () {
+ this.inputElement_.disabled = false;
+ this.updateClasses_();
+};
+MaterialIconToggle.prototype['enable'] = MaterialIconToggle.prototype.enable;
+/**
+ * Check icon toggle.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.check = function () {
+ this.inputElement_.checked = true;
+ this.updateClasses_();
+};
+MaterialIconToggle.prototype['check'] = MaterialIconToggle.prototype.check;
+/**
+ * Uncheck icon toggle.
+ *
+ * @public
+ */
+MaterialIconToggle.prototype.uncheck = function () {
+ this.inputElement_.checked = false;
+ this.updateClasses_();
+};
+MaterialIconToggle.prototype['uncheck'] = MaterialIconToggle.prototype.uncheck;
+/**
+ * Initialize element.
+ */
+MaterialIconToggle.prototype.init = function () {
+ if (this.element_) {
+ this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT);
+ if (this.element_.classList.contains(this.CssClasses_.JS_RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ this.rippleContainerElement_ = document.createElement('span');
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CONTAINER);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.JS_RIPPLE_EFFECT);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER);
+ this.boundRippleMouseUp = this.onMouseUp_.bind(this);
+ this.rippleContainerElement_.addEventListener('mouseup', this.boundRippleMouseUp);
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.RIPPLE);
+ this.rippleContainerElement_.appendChild(ripple);
+ this.element_.appendChild(this.rippleContainerElement_);
+ }
+ this.boundInputOnChange = this.onChange_.bind(this);
+ this.boundInputOnFocus = this.onFocus_.bind(this);
+ this.boundInputOnBlur = this.onBlur_.bind(this);
+ this.boundElementOnMouseUp = this.onMouseUp_.bind(this);
+ this.inputElement_.addEventListener('change', this.boundInputOnChange);
+ this.inputElement_.addEventListener('focus', this.boundInputOnFocus);
+ this.inputElement_.addEventListener('blur', this.boundInputOnBlur);
+ this.element_.addEventListener('mouseup', this.boundElementOnMouseUp);
+ this.updateClasses_();
+ this.element_.classList.add('is-upgraded');
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialIconToggle,
+ classAsString: 'MaterialIconToggle',
+ cssClass: 'mdl-js-icon-toggle',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for dropdown MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialMenu = function MaterialMenu(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialMenu'] = MaterialMenu;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialMenu.prototype.Constant_ = {
+ // Total duration of the menu animation.
+ TRANSITION_DURATION_SECONDS: 0.3,
+ // The fraction of the total duration we want to use for menu item animations.
+ TRANSITION_DURATION_FRACTION: 0.8,
+ // How long the menu stays open after choosing an option (so the user can see
+ // the ripple).
+ CLOSE_TIMEOUT: 150
+};
+/**
+ * Keycodes, for code readability.
+ *
+ * @enum {number}
+ * @private
+ */
+MaterialMenu.prototype.Keycodes_ = {
+ ENTER: 13,
+ ESCAPE: 27,
+ SPACE: 32,
+ UP_ARROW: 38,
+ DOWN_ARROW: 40
+};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialMenu.prototype.CssClasses_ = {
+ CONTAINER: 'mdl-menu__container',
+ OUTLINE: 'mdl-menu__outline',
+ ITEM: 'mdl-menu__item',
+ ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container',
+ RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ RIPPLE: 'mdl-ripple',
+ // Statuses
+ IS_UPGRADED: 'is-upgraded',
+ IS_VISIBLE: 'is-visible',
+ IS_ANIMATING: 'is-animating',
+ // Alignment options
+ BOTTOM_LEFT: 'mdl-menu--bottom-left',
+ // This is the default.
+ BOTTOM_RIGHT: 'mdl-menu--bottom-right',
+ TOP_LEFT: 'mdl-menu--top-left',
+ TOP_RIGHT: 'mdl-menu--top-right',
+ UNALIGNED: 'mdl-menu--unaligned'
+};
+/**
+ * Initialize element.
+ */
+MaterialMenu.prototype.init = function () {
+ if (this.element_) {
+ // Create container for the menu.
+ var container = document.createElement('div');
+ container.classList.add(this.CssClasses_.CONTAINER);
+ this.element_.parentElement.insertBefore(container, this.element_);
+ this.element_.parentElement.removeChild(this.element_);
+ container.appendChild(this.element_);
+ this.container_ = container;
+ // Create outline for the menu (shadow and background).
+ var outline = document.createElement('div');
+ outline.classList.add(this.CssClasses_.OUTLINE);
+ this.outline_ = outline;
+ container.insertBefore(outline, this.element_);
+ // Find the "for" element and bind events to it.
+ var forElId = this.element_.getAttribute('for') || this.element_.getAttribute('data-mdl-for');
+ var forEl = null;
+ if (forElId) {
+ forEl = document.getElementById(forElId);
+ if (forEl) {
+ this.forElement_ = forEl;
+ forEl.addEventListener('click', this.handleForClick_.bind(this));
+ forEl.addEventListener('keydown', this.handleForKeyboardEvent_.bind(this));
+ }
+ }
+ var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
+ this.boundItemKeydown_ = this.handleItemKeyboardEvent_.bind(this);
+ this.boundItemClick_ = this.handleItemClick_.bind(this);
+ for (var i = 0; i < items.length; i++) {
+ // Add a listener to each menu item.
+ items[i].addEventListener('click', this.boundItemClick_);
+ // Add a tab index to each menu item.
+ items[i].tabIndex = '-1';
+ // Add a keyboard listener to each menu item.
+ items[i].addEventListener('keydown', this.boundItemKeydown_);
+ }
+ // Add ripple classes to each item, if the user has enabled ripples.
+ if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ for (i = 0; i < items.length; i++) {
+ var item = items[i];
+ var rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.RIPPLE);
+ rippleContainer.appendChild(ripple);
+ item.appendChild(rippleContainer);
+ item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
+ }
+ }
+ // Copy alignment classes to the container, so the outline can use them.
+ if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
+ this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
+ }
+ if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
+ this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
+ }
+ if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
+ this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
+ }
+ if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
+ this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
+ }
+ if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
+ this.outline_.classList.add(this.CssClasses_.UNALIGNED);
+ }
+ container.classList.add(this.CssClasses_.IS_UPGRADED);
+ }
+};
+/**
+ * Handles a click on the "for" element, by positioning the menu and then
+ * toggling it.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialMenu.prototype.handleForClick_ = function (evt) {
+ if (this.element_ && this.forElement_) {
+ var rect = this.forElement_.getBoundingClientRect();
+ var forRect = this.forElement_.parentElement.getBoundingClientRect();
+ if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
+ } else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
+ // Position below the "for" element, aligned to its right.
+ this.container_.style.right = forRect.right - rect.right + 'px';
+ this.container_.style.top = this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
+ } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
+ // Position above the "for" element, aligned to its left.
+ this.container_.style.left = this.forElement_.offsetLeft + 'px';
+ this.container_.style.bottom = forRect.bottom - rect.top + 'px';
+ } else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
+ // Position above the "for" element, aligned to its right.
+ this.container_.style.right = forRect.right - rect.right + 'px';
+ this.container_.style.bottom = forRect.bottom - rect.top + 'px';
+ } else {
+ // Default: position below the "for" element, aligned to its left.
+ this.container_.style.left = this.forElement_.offsetLeft + 'px';
+ this.container_.style.top = this.forElement_.offsetTop + this.forElement_.offsetHeight + 'px';
+ }
+ }
+ this.toggle(evt);
+};
+/**
+ * Handles a keyboard event on the "for" element.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialMenu.prototype.handleForKeyboardEvent_ = function (evt) {
+ if (this.element_ && this.container_ && this.forElement_) {
+ var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM + ':not([disabled])');
+ if (items && items.length > 0 && this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
+ if (evt.keyCode === this.Keycodes_.UP_ARROW) {
+ evt.preventDefault();
+ items[items.length - 1].focus();
+ } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
+ evt.preventDefault();
+ items[0].focus();
+ }
+ }
+ }
+};
+/**
+ * Handles a keyboard event on an item.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialMenu.prototype.handleItemKeyboardEvent_ = function (evt) {
+ if (this.element_ && this.container_) {
+ var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM + ':not([disabled])');
+ if (items && items.length > 0 && this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
+ var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
+ if (evt.keyCode === this.Keycodes_.UP_ARROW) {
+ evt.preventDefault();
+ if (currentIndex > 0) {
+ items[currentIndex - 1].focus();
+ } else {
+ items[items.length - 1].focus();
+ }
+ } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
+ evt.preventDefault();
+ if (items.length > currentIndex + 1) {
+ items[currentIndex + 1].focus();
+ } else {
+ items[0].focus();
+ }
+ } else if (evt.keyCode === this.Keycodes_.SPACE || evt.keyCode === this.Keycodes_.ENTER) {
+ evt.preventDefault();
+ // Send mousedown and mouseup to trigger ripple.
+ var e = new MouseEvent('mousedown');
+ evt.target.dispatchEvent(e);
+ e = new MouseEvent('mouseup');
+ evt.target.dispatchEvent(e);
+ // Send click.
+ evt.target.click();
+ } else if (evt.keyCode === this.Keycodes_.ESCAPE) {
+ evt.preventDefault();
+ this.hide();
+ }
+ }
+ }
+};
+/**
+ * Handles a click event on an item.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialMenu.prototype.handleItemClick_ = function (evt) {
+ if (evt.target.hasAttribute('disabled')) {
+ evt.stopPropagation();
+ } else {
+ // Wait some time before closing menu, so the user can see the ripple.
+ this.closing_ = true;
+ window.setTimeout(function (evt) {
+ this.hide();
+ this.closing_ = false;
+ }.bind(this), this.Constant_.CLOSE_TIMEOUT);
+ }
+};
+/**
+ * Calculates the initial clip (for opening the menu) or final clip (for closing
+ * it), and applies it. This allows us to animate from or to the correct point,
+ * that is, the point it's aligned to in the "for" element.
+ *
+ * @param {number} height Height of the clip rectangle
+ * @param {number} width Width of the clip rectangle
+ * @private
+ */
+MaterialMenu.prototype.applyClip_ = function (height, width) {
+ if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
+ // Do not clip.
+ this.element_.style.clip = '';
+ } else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
+ // Clip to the top right corner of the menu.
+ this.element_.style.clip = 'rect(0 ' + width + 'px ' + '0 ' + width + 'px)';
+ } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
+ // Clip to the bottom left corner of the menu.
+ this.element_.style.clip = 'rect(' + height + 'px 0 ' + height + 'px 0)';
+ } else if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
+ // Clip to the bottom right corner of the menu.
+ this.element_.style.clip = 'rect(' + height + 'px ' + width + 'px ' + height + 'px ' + width + 'px)';
+ } else {
+ // Default: do not clip (same as clipping to the top left corner).
+ this.element_.style.clip = '';
+ }
+};
+/**
+ * Cleanup function to remove animation listeners.
+ *
+ * @param {Event} evt
+ * @private
+ */
+MaterialMenu.prototype.removeAnimationEndListener_ = function (evt) {
+ evt.target.classList.remove(MaterialMenu.prototype.CssClasses_.IS_ANIMATING);
+};
+/**
+ * Adds an event listener to clean up after the animation ends.
+ *
+ * @private
+ */
+MaterialMenu.prototype.addAnimationEndListener_ = function () {
+ this.element_.addEventListener('transitionend', this.removeAnimationEndListener_);
+ this.element_.addEventListener('webkitTransitionEnd', this.removeAnimationEndListener_);
+};
+/**
+ * Displays the menu.
+ *
+ * @public
+ */
+MaterialMenu.prototype.show = function (evt) {
+ if (this.element_ && this.container_ && this.outline_) {
+ // Measure the inner element.
+ var height = this.element_.getBoundingClientRect().height;
+ var width = this.element_.getBoundingClientRect().width;
+ // Apply the inner element's size to the container and outline.
+ this.container_.style.width = width + 'px';
+ this.container_.style.height = height + 'px';
+ this.outline_.style.width = width + 'px';
+ this.outline_.style.height = height + 'px';
+ var transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS * this.Constant_.TRANSITION_DURATION_FRACTION;
+ // Calculate transition delays for individual menu items, so that they fade
+ // in one at a time.
+ var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
+ for (var i = 0; i < items.length; i++) {
+ var itemDelay = null;
+ if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) || this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
+ itemDelay = (height - items[i].offsetTop - items[i].offsetHeight) / height * transitionDuration + 's';
+ } else {
+ itemDelay = items[i].offsetTop / height * transitionDuration + 's';
+ }
+ items[i].style.transitionDelay = itemDelay;
+ }
+ // Apply the initial clip to the text before we start animating.
+ this.applyClip_(height, width);
+ // Wait for the next frame, turn on animation, and apply the final clip.
+ // Also make it visible. This triggers the transitions.
+ window.requestAnimationFrame(function () {
+ this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
+ this.element_.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)';
+ this.container_.classList.add(this.CssClasses_.IS_VISIBLE);
+ }.bind(this));
+ // Clean up after the animation is complete.
+ this.addAnimationEndListener_();
+ // Add a click listener to the document, to close the menu.
+ var callback = function (e) {
+ // Check to see if the document is processing the same event that
+ // displayed the menu in the first place. If so, do nothing.
+ // Also check to see if the menu is in the process of closing itself, and
+ // do nothing in that case.
+ // Also check if the clicked element is a menu item
+ // if so, do nothing.
+ if (e !== evt && !this.closing_ && e.target.parentNode !== this.element_) {
+ document.removeEventListener('click', callback);
+ this.hide();
+ }
+ }.bind(this);
+ document.addEventListener('click', callback);
+ }
+};
+MaterialMenu.prototype['show'] = MaterialMenu.prototype.show;
+/**
+ * Hides the menu.
+ *
+ * @public
+ */
+MaterialMenu.prototype.hide = function () {
+ if (this.element_ && this.container_ && this.outline_) {
+ var items = this.element_.querySelectorAll('.' + this.CssClasses_.ITEM);
+ // Remove all transition delays; menu items fade out concurrently.
+ for (var i = 0; i < items.length; i++) {
+ items[i].style.removeProperty('transition-delay');
+ }
+ // Measure the inner element.
+ var rect = this.element_.getBoundingClientRect();
+ var height = rect.height;
+ var width = rect.width;
+ // Turn on animation, and apply the final clip. Also make invisible.
+ // This triggers the transitions.
+ this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
+ this.applyClip_(height, width);
+ this.container_.classList.remove(this.CssClasses_.IS_VISIBLE);
+ // Clean up after the animation is complete.
+ this.addAnimationEndListener_();
+ }
+};
+MaterialMenu.prototype['hide'] = MaterialMenu.prototype.hide;
+/**
+ * Displays or hides the menu, depending on current state.
+ *
+ * @public
+ */
+MaterialMenu.prototype.toggle = function (evt) {
+ if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
+ this.hide();
+ } else {
+ this.show(evt);
+ }
+};
+MaterialMenu.prototype['toggle'] = MaterialMenu.prototype.toggle;
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialMenu,
+ classAsString: 'MaterialMenu',
+ cssClass: 'mdl-js-menu',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Progress MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialProgress = function MaterialProgress(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialProgress'] = MaterialProgress;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialProgress.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialProgress.prototype.CssClasses_ = { INDETERMINATE_CLASS: 'mdl-progress__indeterminate' };
+/**
+ * Set the current progress of the progressbar.
+ *
+ * @param {number} p Percentage of the progress (0-100)
+ * @public
+ */
+MaterialProgress.prototype.setProgress = function (p) {
+ if (this.element_.classList.contains(this.CssClasses_.INDETERMINATE_CLASS)) {
+ return;
+ }
+ this.progressbar_.style.width = p + '%';
+};
+MaterialProgress.prototype['setProgress'] = MaterialProgress.prototype.setProgress;
+/**
+ * Set the current progress of the buffer.
+ *
+ * @param {number} p Percentage of the buffer (0-100)
+ * @public
+ */
+MaterialProgress.prototype.setBuffer = function (p) {
+ this.bufferbar_.style.width = p + '%';
+ this.auxbar_.style.width = 100 - p + '%';
+};
+MaterialProgress.prototype['setBuffer'] = MaterialProgress.prototype.setBuffer;
+/**
+ * Initialize element.
+ */
+MaterialProgress.prototype.init = function () {
+ if (this.element_) {
+ var el = document.createElement('div');
+ el.className = 'progressbar bar bar1';
+ this.element_.appendChild(el);
+ this.progressbar_ = el;
+ el = document.createElement('div');
+ el.className = 'bufferbar bar bar2';
+ this.element_.appendChild(el);
+ this.bufferbar_ = el;
+ el = document.createElement('div');
+ el.className = 'auxbar bar bar3';
+ this.element_.appendChild(el);
+ this.auxbar_ = el;
+ this.progressbar_.style.width = '0%';
+ this.bufferbar_.style.width = '100%';
+ this.auxbar_.style.width = '0%';
+ this.element_.classList.add('is-upgraded');
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialProgress,
+ classAsString: 'MaterialProgress',
+ cssClass: 'mdl-js-progress',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Radio MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialRadio = function MaterialRadio(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialRadio'] = MaterialRadio;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialRadio.prototype.Constant_ = { TINY_TIMEOUT: 0.001 };
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialRadio.prototype.CssClasses_ = {
+ IS_FOCUSED: 'is-focused',
+ IS_DISABLED: 'is-disabled',
+ IS_CHECKED: 'is-checked',
+ IS_UPGRADED: 'is-upgraded',
+ JS_RADIO: 'mdl-js-radio',
+ RADIO_BTN: 'mdl-radio__button',
+ RADIO_OUTER_CIRCLE: 'mdl-radio__outer-circle',
+ RADIO_INNER_CIRCLE: 'mdl-radio__inner-circle',
+ RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ RIPPLE_CONTAINER: 'mdl-radio__ripple-container',
+ RIPPLE_CENTER: 'mdl-ripple--center',
+ RIPPLE: 'mdl-ripple'
+};
+/**
+ * Handle change of state.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialRadio.prototype.onChange_ = function (event) {
+ // Since other radio buttons don't get change events, we need to look for
+ // them to update their classes.
+ var radios = document.getElementsByClassName(this.CssClasses_.JS_RADIO);
+ for (var i = 0; i < radios.length; i++) {
+ var button = radios[i].querySelector('.' + this.CssClasses_.RADIO_BTN);
+ // Different name == different group, so no point updating those.
+ if (button.getAttribute('name') === this.btnElement_.getAttribute('name')) {
+ radios[i]['MaterialRadio'].updateClasses_();
+ }
+ }
+};
+/**
+ * Handle focus.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialRadio.prototype.onFocus_ = function (event) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle lost focus.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialRadio.prototype.onBlur_ = function (event) {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle mouseup.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialRadio.prototype.onMouseup_ = function (event) {
+ this.blur_();
+};
+/**
+ * Update classes.
+ *
+ * @private
+ */
+MaterialRadio.prototype.updateClasses_ = function () {
+ this.checkDisabled();
+ this.checkToggleState();
+};
+/**
+ * Add blur.
+ *
+ * @private
+ */
+MaterialRadio.prototype.blur_ = function () {
+ // TODO: figure out why there's a focus event being fired after our blur,
+ // so that we can avoid this hack.
+ window.setTimeout(function () {
+ this.btnElement_.blur();
+ }.bind(this), this.Constant_.TINY_TIMEOUT);
+};
+// Public methods.
+/**
+ * Check the components disabled state.
+ *
+ * @public
+ */
+MaterialRadio.prototype.checkDisabled = function () {
+ if (this.btnElement_.disabled) {
+ this.element_.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+};
+MaterialRadio.prototype['checkDisabled'] = MaterialRadio.prototype.checkDisabled;
+/**
+ * Check the components toggled state.
+ *
+ * @public
+ */
+MaterialRadio.prototype.checkToggleState = function () {
+ if (this.btnElement_.checked) {
+ this.element_.classList.add(this.CssClasses_.IS_CHECKED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_CHECKED);
+ }
+};
+MaterialRadio.prototype['checkToggleState'] = MaterialRadio.prototype.checkToggleState;
+/**
+ * Disable radio.
+ *
+ * @public
+ */
+MaterialRadio.prototype.disable = function () {
+ this.btnElement_.disabled = true;
+ this.updateClasses_();
+};
+MaterialRadio.prototype['disable'] = MaterialRadio.prototype.disable;
+/**
+ * Enable radio.
+ *
+ * @public
+ */
+MaterialRadio.prototype.enable = function () {
+ this.btnElement_.disabled = false;
+ this.updateClasses_();
+};
+MaterialRadio.prototype['enable'] = MaterialRadio.prototype.enable;
+/**
+ * Check radio.
+ *
+ * @public
+ */
+MaterialRadio.prototype.check = function () {
+ this.btnElement_.checked = true;
+ this.onChange_(null);
+};
+MaterialRadio.prototype['check'] = MaterialRadio.prototype.check;
+/**
+ * Uncheck radio.
+ *
+ * @public
+ */
+MaterialRadio.prototype.uncheck = function () {
+ this.btnElement_.checked = false;
+ this.onChange_(null);
+};
+MaterialRadio.prototype['uncheck'] = MaterialRadio.prototype.uncheck;
+/**
+ * Initialize element.
+ */
+MaterialRadio.prototype.init = function () {
+ if (this.element_) {
+ this.btnElement_ = this.element_.querySelector('.' + this.CssClasses_.RADIO_BTN);
+ this.boundChangeHandler_ = this.onChange_.bind(this);
+ this.boundFocusHandler_ = this.onChange_.bind(this);
+ this.boundBlurHandler_ = this.onBlur_.bind(this);
+ this.boundMouseUpHandler_ = this.onMouseup_.bind(this);
+ var outerCircle = document.createElement('span');
+ outerCircle.classList.add(this.CssClasses_.RADIO_OUTER_CIRCLE);
+ var innerCircle = document.createElement('span');
+ innerCircle.classList.add(this.CssClasses_.RADIO_INNER_CIRCLE);
+ this.element_.appendChild(outerCircle);
+ this.element_.appendChild(innerCircle);
+ var rippleContainer;
+ if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(this.CssClasses_.RIPPLE_CONTAINER);
+ rippleContainer.classList.add(this.CssClasses_.RIPPLE_EFFECT);
+ rippleContainer.classList.add(this.CssClasses_.RIPPLE_CENTER);
+ rippleContainer.addEventListener('mouseup', this.boundMouseUpHandler_);
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.RIPPLE);
+ rippleContainer.appendChild(ripple);
+ this.element_.appendChild(rippleContainer);
+ }
+ this.btnElement_.addEventListener('change', this.boundChangeHandler_);
+ this.btnElement_.addEventListener('focus', this.boundFocusHandler_);
+ this.btnElement_.addEventListener('blur', this.boundBlurHandler_);
+ this.element_.addEventListener('mouseup', this.boundMouseUpHandler_);
+ this.updateClasses_();
+ this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialRadio,
+ classAsString: 'MaterialRadio',
+ cssClass: 'mdl-js-radio',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Slider MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialSlider = function MaterialSlider(element) {
+ this.element_ = element;
+ // Browser feature detection.
+ this.isIE_ = window.navigator.msPointerEnabled;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialSlider'] = MaterialSlider;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialSlider.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialSlider.prototype.CssClasses_ = {
+ IE_CONTAINER: 'mdl-slider__ie-container',
+ SLIDER_CONTAINER: 'mdl-slider__container',
+ BACKGROUND_FLEX: 'mdl-slider__background-flex',
+ BACKGROUND_LOWER: 'mdl-slider__background-lower',
+ BACKGROUND_UPPER: 'mdl-slider__background-upper',
+ IS_LOWEST_VALUE: 'is-lowest-value',
+ IS_UPGRADED: 'is-upgraded'
+};
+/**
+ * Handle input on element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSlider.prototype.onInput_ = function (event) {
+ this.updateValueStyles_();
+};
+/**
+ * Handle change on element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSlider.prototype.onChange_ = function (event) {
+ this.updateValueStyles_();
+};
+/**
+ * Handle mouseup on element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSlider.prototype.onMouseUp_ = function (event) {
+ event.target.blur();
+};
+/**
+ * Handle mousedown on container element.
+ * This handler is purpose is to not require the use to click
+ * exactly on the 2px slider element, as FireFox seems to be very
+ * strict about this.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ * @suppress {missingProperties}
+ */
+MaterialSlider.prototype.onContainerMouseDown_ = function (event) {
+ // If this click is not on the parent element (but rather some child)
+ // ignore. It may still bubble up.
+ if (event.target !== this.element_.parentElement) {
+ return;
+ }
+ // Discard the original event and create a new event that
+ // is on the slider element.
+ event.preventDefault();
+ var newEvent = new MouseEvent('mousedown', {
+ target: event.target,
+ buttons: event.buttons,
+ clientX: event.clientX,
+ clientY: this.element_.getBoundingClientRect().y
+ });
+ this.element_.dispatchEvent(newEvent);
+};
+/**
+ * Handle updating of values.
+ *
+ * @private
+ */
+MaterialSlider.prototype.updateValueStyles_ = function () {
+ // Calculate and apply percentages to div structure behind slider.
+ var fraction = (this.element_.value - this.element_.min) / (this.element_.max - this.element_.min);
+ if (fraction === 0) {
+ this.element_.classList.add(this.CssClasses_.IS_LOWEST_VALUE);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_LOWEST_VALUE);
+ }
+ if (!this.isIE_) {
+ this.backgroundLower_.style.flex = fraction;
+ this.backgroundLower_.style.webkitFlex = fraction;
+ this.backgroundUpper_.style.flex = 1 - fraction;
+ this.backgroundUpper_.style.webkitFlex = 1 - fraction;
+ }
+};
+// Public methods.
+/**
+ * Disable slider.
+ *
+ * @public
+ */
+MaterialSlider.prototype.disable = function () {
+ this.element_.disabled = true;
+};
+MaterialSlider.prototype['disable'] = MaterialSlider.prototype.disable;
+/**
+ * Enable slider.
+ *
+ * @public
+ */
+MaterialSlider.prototype.enable = function () {
+ this.element_.disabled = false;
+};
+MaterialSlider.prototype['enable'] = MaterialSlider.prototype.enable;
+/**
+ * Update slider value.
+ *
+ * @param {number} value The value to which to set the control (optional).
+ * @public
+ */
+MaterialSlider.prototype.change = function (value) {
+ if (typeof value !== 'undefined') {
+ this.element_.value = value;
+ }
+ this.updateValueStyles_();
+};
+MaterialSlider.prototype['change'] = MaterialSlider.prototype.change;
+/**
+ * Initialize element.
+ */
+MaterialSlider.prototype.init = function () {
+ if (this.element_) {
+ if (this.isIE_) {
+ // Since we need to specify a very large height in IE due to
+ // implementation limitations, we add a parent here that trims it down to
+ // a reasonable size.
+ var containerIE = document.createElement('div');
+ containerIE.classList.add(this.CssClasses_.IE_CONTAINER);
+ this.element_.parentElement.insertBefore(containerIE, this.element_);
+ this.element_.parentElement.removeChild(this.element_);
+ containerIE.appendChild(this.element_);
+ } else {
+ // For non-IE browsers, we need a div structure that sits behind the
+ // slider and allows us to style the left and right sides of it with
+ // different colors.
+ var container = document.createElement('div');
+ container.classList.add(this.CssClasses_.SLIDER_CONTAINER);
+ this.element_.parentElement.insertBefore(container, this.element_);
+ this.element_.parentElement.removeChild(this.element_);
+ container.appendChild(this.element_);
+ var backgroundFlex = document.createElement('div');
+ backgroundFlex.classList.add(this.CssClasses_.BACKGROUND_FLEX);
+ container.appendChild(backgroundFlex);
+ this.backgroundLower_ = document.createElement('div');
+ this.backgroundLower_.classList.add(this.CssClasses_.BACKGROUND_LOWER);
+ backgroundFlex.appendChild(this.backgroundLower_);
+ this.backgroundUpper_ = document.createElement('div');
+ this.backgroundUpper_.classList.add(this.CssClasses_.BACKGROUND_UPPER);
+ backgroundFlex.appendChild(this.backgroundUpper_);
+ }
+ this.boundInputHandler = this.onInput_.bind(this);
+ this.boundChangeHandler = this.onChange_.bind(this);
+ this.boundMouseUpHandler = this.onMouseUp_.bind(this);
+ this.boundContainerMouseDownHandler = this.onContainerMouseDown_.bind(this);
+ this.element_.addEventListener('input', this.boundInputHandler);
+ this.element_.addEventListener('change', this.boundChangeHandler);
+ this.element_.addEventListener('mouseup', this.boundMouseUpHandler);
+ this.element_.parentElement.addEventListener('mousedown', this.boundContainerMouseDownHandler);
+ this.updateValueStyles_();
+ this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialSlider,
+ classAsString: 'MaterialSlider',
+ cssClass: 'mdl-js-slider',
+ widget: true
+});
+/**
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Snackbar MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialSnackbar = function MaterialSnackbar(element) {
+ this.element_ = element;
+ this.textElement_ = this.element_.querySelector('.' + this.cssClasses_.MESSAGE);
+ this.actionElement_ = this.element_.querySelector('.' + this.cssClasses_.ACTION);
+ if (!this.textElement_) {
+ throw new Error('There must be a message element for a snackbar.');
+ }
+ if (!this.actionElement_) {
+ throw new Error('There must be an action element for a snackbar.');
+ }
+ this.active = false;
+ this.actionHandler_ = undefined;
+ this.message_ = undefined;
+ this.actionText_ = undefined;
+ this.queuedNotifications_ = [];
+ this.setActionHidden_(true);
+};
+window['MaterialSnackbar'] = MaterialSnackbar;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialSnackbar.prototype.Constant_ = {
+ // The duration of the snackbar show/hide animation, in ms.
+ ANIMATION_LENGTH: 250
+};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialSnackbar.prototype.cssClasses_ = {
+ SNACKBAR: 'mdl-snackbar',
+ MESSAGE: 'mdl-snackbar__text',
+ ACTION: 'mdl-snackbar__action',
+ ACTIVE: 'mdl-snackbar--active'
+};
+/**
+ * Display the snackbar.
+ *
+ * @private
+ */
+MaterialSnackbar.prototype.displaySnackbar_ = function () {
+ this.element_.setAttribute('aria-hidden', 'true');
+ if (this.actionHandler_) {
+ this.actionElement_.textContent = this.actionText_;
+ this.actionElement_.addEventListener('click', this.actionHandler_);
+ this.setActionHidden_(false);
+ }
+ this.textElement_.textContent = this.message_;
+ this.element_.classList.add(this.cssClasses_.ACTIVE);
+ this.element_.setAttribute('aria-hidden', 'false');
+ setTimeout(this.cleanup_.bind(this), this.timeout_);
+};
+/**
+ * Show the snackbar.
+ *
+ * @param {Object} data The data for the notification.
+ * @public
+ */
+MaterialSnackbar.prototype.showSnackbar = function (data) {
+ if (data === undefined) {
+ throw new Error('Please provide a data object with at least a message to display.');
+ }
+ if (data['message'] === undefined) {
+ throw new Error('Please provide a message to be displayed.');
+ }
+ if (data['actionHandler'] && !data['actionText']) {
+ throw new Error('Please provide action text with the handler.');
+ }
+ if (this.active) {
+ this.queuedNotifications_.push(data);
+ } else {
+ this.active = true;
+ this.message_ = data['message'];
+ if (data['timeout']) {
+ this.timeout_ = data['timeout'];
+ } else {
+ this.timeout_ = 2750;
+ }
+ if (data['actionHandler']) {
+ this.actionHandler_ = data['actionHandler'];
+ }
+ if (data['actionText']) {
+ this.actionText_ = data['actionText'];
+ }
+ this.displaySnackbar_();
+ }
+};
+MaterialSnackbar.prototype['showSnackbar'] = MaterialSnackbar.prototype.showSnackbar;
+/**
+ * Check if the queue has items within it.
+ * If it does, display the next entry.
+ *
+ * @private
+ */
+MaterialSnackbar.prototype.checkQueue_ = function () {
+ if (this.queuedNotifications_.length > 0) {
+ this.showSnackbar(this.queuedNotifications_.shift());
+ }
+};
+/**
+ * Cleanup the snackbar event listeners and accessiblity attributes.
+ *
+ * @private
+ */
+MaterialSnackbar.prototype.cleanup_ = function () {
+ this.element_.classList.remove(this.cssClasses_.ACTIVE);
+ setTimeout(function () {
+ this.element_.setAttribute('aria-hidden', 'true');
+ this.textElement_.textContent = '';
+ if (!Boolean(this.actionElement_.getAttribute('aria-hidden'))) {
+ this.setActionHidden_(true);
+ this.actionElement_.textContent = '';
+ this.actionElement_.removeEventListener('click', this.actionHandler_);
+ }
+ this.actionHandler_ = undefined;
+ this.message_ = undefined;
+ this.actionText_ = undefined;
+ this.active = false;
+ this.checkQueue_();
+ }.bind(this), this.Constant_.ANIMATION_LENGTH);
+};
+/**
+ * Set the action handler hidden state.
+ *
+ * @param {boolean} value
+ * @private
+ */
+MaterialSnackbar.prototype.setActionHidden_ = function (value) {
+ if (value) {
+ this.actionElement_.setAttribute('aria-hidden', 'true');
+ } else {
+ this.actionElement_.removeAttribute('aria-hidden');
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialSnackbar,
+ classAsString: 'MaterialSnackbar',
+ cssClass: 'mdl-js-snackbar',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Spinner MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @param {HTMLElement} element The element that will be upgraded.
+ * @constructor
+ */
+var MaterialSpinner = function MaterialSpinner(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialSpinner'] = MaterialSpinner;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialSpinner.prototype.Constant_ = { MDL_SPINNER_LAYER_COUNT: 4 };
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialSpinner.prototype.CssClasses_ = {
+ MDL_SPINNER_LAYER: 'mdl-spinner__layer',
+ MDL_SPINNER_CIRCLE_CLIPPER: 'mdl-spinner__circle-clipper',
+ MDL_SPINNER_CIRCLE: 'mdl-spinner__circle',
+ MDL_SPINNER_GAP_PATCH: 'mdl-spinner__gap-patch',
+ MDL_SPINNER_LEFT: 'mdl-spinner__left',
+ MDL_SPINNER_RIGHT: 'mdl-spinner__right'
+};
+/**
+ * Auxiliary method to create a spinner layer.
+ *
+ * @param {number} index Index of the layer to be created.
+ * @public
+ */
+MaterialSpinner.prototype.createLayer = function (index) {
+ var layer = document.createElement('div');
+ layer.classList.add(this.CssClasses_.MDL_SPINNER_LAYER);
+ layer.classList.add(this.CssClasses_.MDL_SPINNER_LAYER + '-' + index);
+ var leftClipper = document.createElement('div');
+ leftClipper.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER);
+ leftClipper.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);
+ var gapPatch = document.createElement('div');
+ gapPatch.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);
+ var rightClipper = document.createElement('div');
+ rightClipper.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER);
+ rightClipper.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);
+ var circleOwners = [
+ leftClipper,
+ gapPatch,
+ rightClipper
+ ];
+ for (var i = 0; i < circleOwners.length; i++) {
+ var circle = document.createElement('div');
+ circle.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE);
+ circleOwners[i].appendChild(circle);
+ }
+ layer.appendChild(leftClipper);
+ layer.appendChild(gapPatch);
+ layer.appendChild(rightClipper);
+ this.element_.appendChild(layer);
+};
+MaterialSpinner.prototype['createLayer'] = MaterialSpinner.prototype.createLayer;
+/**
+ * Stops the spinner animation.
+ * Public method for users who need to stop the spinner for any reason.
+ *
+ * @public
+ */
+MaterialSpinner.prototype.stop = function () {
+ this.element_.classList.remove('is-active');
+};
+MaterialSpinner.prototype['stop'] = MaterialSpinner.prototype.stop;
+/**
+ * Starts the spinner animation.
+ * Public method for users who need to manually start the spinner for any reason
+ * (instead of just adding the 'is-active' class to their markup).
+ *
+ * @public
+ */
+MaterialSpinner.prototype.start = function () {
+ this.element_.classList.add('is-active');
+};
+MaterialSpinner.prototype['start'] = MaterialSpinner.prototype.start;
+/**
+ * Initialize element.
+ */
+MaterialSpinner.prototype.init = function () {
+ if (this.element_) {
+ for (var i = 1; i <= this.Constant_.MDL_SPINNER_LAYER_COUNT; i++) {
+ this.createLayer(i);
+ }
+ this.element_.classList.add('is-upgraded');
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialSpinner,
+ classAsString: 'MaterialSpinner',
+ cssClass: 'mdl-js-spinner',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Checkbox MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialSwitch = function MaterialSwitch(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialSwitch'] = MaterialSwitch;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialSwitch.prototype.Constant_ = { TINY_TIMEOUT: 0.001 };
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialSwitch.prototype.CssClasses_ = {
+ INPUT: 'mdl-switch__input',
+ TRACK: 'mdl-switch__track',
+ THUMB: 'mdl-switch__thumb',
+ FOCUS_HELPER: 'mdl-switch__focus-helper',
+ RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ RIPPLE_CONTAINER: 'mdl-switch__ripple-container',
+ RIPPLE_CENTER: 'mdl-ripple--center',
+ RIPPLE: 'mdl-ripple',
+ IS_FOCUSED: 'is-focused',
+ IS_DISABLED: 'is-disabled',
+ IS_CHECKED: 'is-checked'
+};
+/**
+ * Handle change of state.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onChange_ = function (event) {
+ this.updateClasses_();
+};
+/**
+ * Handle focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onFocus_ = function (event) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle lost focus of element.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onBlur_ = function (event) {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle mouseup.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialSwitch.prototype.onMouseUp_ = function (event) {
+ this.blur_();
+};
+/**
+ * Handle class updates.
+ *
+ * @private
+ */
+MaterialSwitch.prototype.updateClasses_ = function () {
+ this.checkDisabled();
+ this.checkToggleState();
+};
+/**
+ * Add blur.
+ *
+ * @private
+ */
+MaterialSwitch.prototype.blur_ = function () {
+ // TODO: figure out why there's a focus event being fired after our blur,
+ // so that we can avoid this hack.
+ window.setTimeout(function () {
+ this.inputElement_.blur();
+ }.bind(this), this.Constant_.TINY_TIMEOUT);
+};
+// Public methods.
+/**
+ * Check the components disabled state.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.checkDisabled = function () {
+ if (this.inputElement_.disabled) {
+ this.element_.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+};
+MaterialSwitch.prototype['checkDisabled'] = MaterialSwitch.prototype.checkDisabled;
+/**
+ * Check the components toggled state.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.checkToggleState = function () {
+ if (this.inputElement_.checked) {
+ this.element_.classList.add(this.CssClasses_.IS_CHECKED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_CHECKED);
+ }
+};
+MaterialSwitch.prototype['checkToggleState'] = MaterialSwitch.prototype.checkToggleState;
+/**
+ * Disable switch.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.disable = function () {
+ this.inputElement_.disabled = true;
+ this.updateClasses_();
+};
+MaterialSwitch.prototype['disable'] = MaterialSwitch.prototype.disable;
+/**
+ * Enable switch.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.enable = function () {
+ this.inputElement_.disabled = false;
+ this.updateClasses_();
+};
+MaterialSwitch.prototype['enable'] = MaterialSwitch.prototype.enable;
+/**
+ * Activate switch.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.on = function () {
+ this.inputElement_.checked = true;
+ this.updateClasses_();
+};
+MaterialSwitch.prototype['on'] = MaterialSwitch.prototype.on;
+/**
+ * Deactivate switch.
+ *
+ * @public
+ */
+MaterialSwitch.prototype.off = function () {
+ this.inputElement_.checked = false;
+ this.updateClasses_();
+};
+MaterialSwitch.prototype['off'] = MaterialSwitch.prototype.off;
+/**
+ * Initialize element.
+ */
+MaterialSwitch.prototype.init = function () {
+ if (this.element_) {
+ this.inputElement_ = this.element_.querySelector('.' + this.CssClasses_.INPUT);
+ var track = document.createElement('div');
+ track.classList.add(this.CssClasses_.TRACK);
+ var thumb = document.createElement('div');
+ thumb.classList.add(this.CssClasses_.THUMB);
+ var focusHelper = document.createElement('span');
+ focusHelper.classList.add(this.CssClasses_.FOCUS_HELPER);
+ thumb.appendChild(focusHelper);
+ this.element_.appendChild(track);
+ this.element_.appendChild(thumb);
+ this.boundMouseUpHandler = this.onMouseUp_.bind(this);
+ if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ this.rippleContainerElement_ = document.createElement('span');
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CONTAINER);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_EFFECT);
+ this.rippleContainerElement_.classList.add(this.CssClasses_.RIPPLE_CENTER);
+ this.rippleContainerElement_.addEventListener('mouseup', this.boundMouseUpHandler);
+ var ripple = document.createElement('span');
+ ripple.classList.add(this.CssClasses_.RIPPLE);
+ this.rippleContainerElement_.appendChild(ripple);
+ this.element_.appendChild(this.rippleContainerElement_);
+ }
+ this.boundChangeHandler = this.onChange_.bind(this);
+ this.boundFocusHandler = this.onFocus_.bind(this);
+ this.boundBlurHandler = this.onBlur_.bind(this);
+ this.inputElement_.addEventListener('change', this.boundChangeHandler);
+ this.inputElement_.addEventListener('focus', this.boundFocusHandler);
+ this.inputElement_.addEventListener('blur', this.boundBlurHandler);
+ this.element_.addEventListener('mouseup', this.boundMouseUpHandler);
+ this.updateClasses_();
+ this.element_.classList.add('is-upgraded');
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialSwitch,
+ classAsString: 'MaterialSwitch',
+ cssClass: 'mdl-js-switch',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Tabs MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {Element} element The element that will be upgraded.
+ */
+var MaterialTabs = function MaterialTabs(element) {
+ // Stores the HTML element.
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialTabs'] = MaterialTabs;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialTabs.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialTabs.prototype.CssClasses_ = {
+ TAB_CLASS: 'mdl-tabs__tab',
+ PANEL_CLASS: 'mdl-tabs__panel',
+ ACTIVE_CLASS: 'is-active',
+ UPGRADED_CLASS: 'is-upgraded',
+ MDL_JS_RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ MDL_RIPPLE_CONTAINER: 'mdl-tabs__ripple-container',
+ MDL_RIPPLE: 'mdl-ripple',
+ MDL_JS_RIPPLE_EFFECT_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events'
+};
+/**
+ * Handle clicks to a tabs component
+ *
+ * @private
+ */
+MaterialTabs.prototype.initTabs_ = function () {
+ if (this.element_.classList.contains(this.CssClasses_.MDL_JS_RIPPLE_EFFECT)) {
+ this.element_.classList.add(this.CssClasses_.MDL_JS_RIPPLE_EFFECT_IGNORE_EVENTS);
+ }
+ // Select element tabs, document panels
+ this.tabs_ = this.element_.querySelectorAll('.' + this.CssClasses_.TAB_CLASS);
+ this.panels_ = this.element_.querySelectorAll('.' + this.CssClasses_.PANEL_CLASS);
+ // Create new tabs for each tab element
+ for (var i = 0; i < this.tabs_.length; i++) {
+ new MaterialTab(this.tabs_[i], this);
+ }
+ this.element_.classList.add(this.CssClasses_.UPGRADED_CLASS);
+};
+/**
+ * Reset tab state, dropping active classes
+ *
+ * @private
+ */
+MaterialTabs.prototype.resetTabState_ = function () {
+ for (var k = 0; k < this.tabs_.length; k++) {
+ this.tabs_[k].classList.remove(this.CssClasses_.ACTIVE_CLASS);
+ }
+};
+/**
+ * Reset panel state, droping active classes
+ *
+ * @private
+ */
+MaterialTabs.prototype.resetPanelState_ = function () {
+ for (var j = 0; j < this.panels_.length; j++) {
+ this.panels_[j].classList.remove(this.CssClasses_.ACTIVE_CLASS);
+ }
+};
+/**
+ * Initialize element.
+ */
+MaterialTabs.prototype.init = function () {
+ if (this.element_) {
+ this.initTabs_();
+ }
+};
+/**
+ * Constructor for an individual tab.
+ *
+ * @constructor
+ * @param {Element} tab The HTML element for the tab.
+ * @param {MaterialTabs} ctx The MaterialTabs object that owns the tab.
+ */
+function MaterialTab(tab, ctx) {
+ if (tab) {
+ if (ctx.element_.classList.contains(ctx.CssClasses_.MDL_JS_RIPPLE_EFFECT)) {
+ var rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(ctx.CssClasses_.MDL_RIPPLE_CONTAINER);
+ rippleContainer.classList.add(ctx.CssClasses_.MDL_JS_RIPPLE_EFFECT);
+ var ripple = document.createElement('span');
+ ripple.classList.add(ctx.CssClasses_.MDL_RIPPLE);
+ rippleContainer.appendChild(ripple);
+ tab.appendChild(rippleContainer);
+ }
+ tab.addEventListener('click', function (e) {
+ if (tab.getAttribute('href').charAt(0) === '#') {
+ e.preventDefault();
+ }
+ var href = tab.href.split('#')[1];
+ var panel = ctx.element_.querySelector('#' + href);
+ ctx.resetTabState_();
+ ctx.resetPanelState_();
+ tab.classList.add(ctx.CssClasses_.ACTIVE_CLASS);
+ if (panel) {
+ panel.classList.add(ctx.CssClasses_.ACTIVE_CLASS);
+ }
+ });
+ }
+}
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialTabs,
+ classAsString: 'MaterialTabs',
+ cssClass: 'mdl-js-tabs'
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Textfield MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialTextfield = function MaterialTextfield(element) {
+ this.element_ = element;
+ this.maxRows = this.Constant_.NO_MAX_ROWS;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialTextfield'] = MaterialTextfield;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialTextfield.prototype.Constant_ = {
+ NO_MAX_ROWS: -1,
+ MAX_ROWS_ATTRIBUTE: 'maxrows'
+};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialTextfield.prototype.CssClasses_ = {
+ LABEL: 'mdl-textfield__label',
+ INPUT: 'mdl-textfield__input',
+ IS_DIRTY: 'is-dirty',
+ IS_FOCUSED: 'is-focused',
+ IS_DISABLED: 'is-disabled',
+ IS_INVALID: 'is-invalid',
+ IS_UPGRADED: 'is-upgraded',
+ HAS_PLACEHOLDER: 'has-placeholder'
+};
+/**
+ * Handle input being entered.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialTextfield.prototype.onKeyDown_ = function (event) {
+ var currentRowCount = event.target.value.split('\n').length;
+ if (event.keyCode === 13) {
+ if (currentRowCount >= this.maxRows) {
+ event.preventDefault();
+ }
+ }
+};
+/**
+ * Handle focus.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialTextfield.prototype.onFocus_ = function (event) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle lost focus.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialTextfield.prototype.onBlur_ = function (event) {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+};
+/**
+ * Handle reset event from out side.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialTextfield.prototype.onReset_ = function (event) {
+ this.updateClasses_();
+};
+/**
+ * Handle class updates.
+ *
+ * @private
+ */
+MaterialTextfield.prototype.updateClasses_ = function () {
+ this.checkDisabled();
+ this.checkValidity();
+ this.checkDirty();
+ this.checkFocus();
+};
+// Public methods.
+/**
+ * Check the disabled state and update field accordingly.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.checkDisabled = function () {
+ if (this.input_.disabled) {
+ this.element_.classList.add(this.CssClasses_.IS_DISABLED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DISABLED);
+ }
+};
+MaterialTextfield.prototype['checkDisabled'] = MaterialTextfield.prototype.checkDisabled;
+/**
+ * Check the focus state and update field accordingly.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.checkFocus = function () {
+ if (Boolean(this.element_.querySelector(':focus'))) {
+ this.element_.classList.add(this.CssClasses_.IS_FOCUSED);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_FOCUSED);
+ }
+};
+MaterialTextfield.prototype['checkFocus'] = MaterialTextfield.prototype.checkFocus;
+/**
+ * Check the validity state and update field accordingly.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.checkValidity = function () {
+ if (this.input_.validity) {
+ if (this.input_.validity.valid) {
+ this.element_.classList.remove(this.CssClasses_.IS_INVALID);
+ } else {
+ this.element_.classList.add(this.CssClasses_.IS_INVALID);
+ }
+ }
+};
+MaterialTextfield.prototype['checkValidity'] = MaterialTextfield.prototype.checkValidity;
+/**
+ * Check the dirty state and update field accordingly.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.checkDirty = function () {
+ if (this.input_.value && this.input_.value.length > 0) {
+ this.element_.classList.add(this.CssClasses_.IS_DIRTY);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_DIRTY);
+ }
+};
+MaterialTextfield.prototype['checkDirty'] = MaterialTextfield.prototype.checkDirty;
+/**
+ * Disable text field.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.disable = function () {
+ this.input_.disabled = true;
+ this.updateClasses_();
+};
+MaterialTextfield.prototype['disable'] = MaterialTextfield.prototype.disable;
+/**
+ * Enable text field.
+ *
+ * @public
+ */
+MaterialTextfield.prototype.enable = function () {
+ this.input_.disabled = false;
+ this.updateClasses_();
+};
+MaterialTextfield.prototype['enable'] = MaterialTextfield.prototype.enable;
+/**
+ * Update text field value.
+ *
+ * @param {string} value The value to which to set the control (optional).
+ * @public
+ */
+MaterialTextfield.prototype.change = function (value) {
+ this.input_.value = value || '';
+ this.updateClasses_();
+};
+MaterialTextfield.prototype['change'] = MaterialTextfield.prototype.change;
+/**
+ * Initialize element.
+ */
+MaterialTextfield.prototype.init = function () {
+ if (this.element_) {
+ this.label_ = this.element_.querySelector('.' + this.CssClasses_.LABEL);
+ this.input_ = this.element_.querySelector('.' + this.CssClasses_.INPUT);
+ if (this.input_) {
+ if (this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)) {
+ this.maxRows = parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE), 10);
+ if (isNaN(this.maxRows)) {
+ this.maxRows = this.Constant_.NO_MAX_ROWS;
+ }
+ }
+ if (this.input_.hasAttribute('placeholder')) {
+ this.element_.classList.add(this.CssClasses_.HAS_PLACEHOLDER);
+ }
+ this.boundUpdateClassesHandler = this.updateClasses_.bind(this);
+ this.boundFocusHandler = this.onFocus_.bind(this);
+ this.boundBlurHandler = this.onBlur_.bind(this);
+ this.boundResetHandler = this.onReset_.bind(this);
+ this.input_.addEventListener('input', this.boundUpdateClassesHandler);
+ this.input_.addEventListener('focus', this.boundFocusHandler);
+ this.input_.addEventListener('blur', this.boundBlurHandler);
+ this.input_.addEventListener('reset', this.boundResetHandler);
+ if (this.maxRows !== this.Constant_.NO_MAX_ROWS) {
+ // TODO: This should handle pasting multi line text.
+ // Currently doesn't.
+ this.boundKeyDownHandler = this.onKeyDown_.bind(this);
+ this.input_.addEventListener('keydown', this.boundKeyDownHandler);
+ }
+ var invalid = this.element_.classList.contains(this.CssClasses_.IS_INVALID);
+ this.updateClasses_();
+ this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
+ if (invalid) {
+ this.element_.classList.add(this.CssClasses_.IS_INVALID);
+ }
+ if (this.input_.hasAttribute('autofocus')) {
+ this.element_.focus();
+ this.checkFocus();
+ }
+ }
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialTextfield,
+ classAsString: 'MaterialTextfield',
+ cssClass: 'mdl-js-textfield',
+ widget: true
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Tooltip MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialTooltip = function MaterialTooltip(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialTooltip'] = MaterialTooltip;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialTooltip.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialTooltip.prototype.CssClasses_ = {
+ IS_ACTIVE: 'is-active',
+ BOTTOM: 'mdl-tooltip--bottom',
+ LEFT: 'mdl-tooltip--left',
+ RIGHT: 'mdl-tooltip--right',
+ TOP: 'mdl-tooltip--top'
+};
+/**
+ * Handle mouseenter for tooltip.
+ *
+ * @param {Event} event The event that fired.
+ * @private
+ */
+MaterialTooltip.prototype.handleMouseEnter_ = function (event) {
+ var props = event.target.getBoundingClientRect();
+ var left = props.left + props.width / 2;
+ var top = props.top + props.height / 2;
+ var marginLeft = -1 * (this.element_.offsetWidth / 2);
+ var marginTop = -1 * (this.element_.offsetHeight / 2);
+ if (this.element_.classList.contains(this.CssClasses_.LEFT) || this.element_.classList.contains(this.CssClasses_.RIGHT)) {
+ left = props.width / 2;
+ if (top + marginTop < 0) {
+ this.element_.style.top = '0';
+ this.element_.style.marginTop = '0';
+ } else {
+ this.element_.style.top = top + 'px';
+ this.element_.style.marginTop = marginTop + 'px';
+ }
+ } else {
+ if (left + marginLeft < 0) {
+ this.element_.style.left = '0';
+ this.element_.style.marginLeft = '0';
+ } else {
+ this.element_.style.left = left + 'px';
+ this.element_.style.marginLeft = marginLeft + 'px';
+ }
+ }
+ if (this.element_.classList.contains(this.CssClasses_.TOP)) {
+ this.element_.style.top = props.top - this.element_.offsetHeight - 10 + 'px';
+ } else if (this.element_.classList.contains(this.CssClasses_.RIGHT)) {
+ this.element_.style.left = props.left + props.width + 10 + 'px';
+ } else if (this.element_.classList.contains(this.CssClasses_.LEFT)) {
+ this.element_.style.left = props.left - this.element_.offsetWidth - 10 + 'px';
+ } else {
+ this.element_.style.top = props.top + props.height + 10 + 'px';
+ }
+ this.element_.classList.add(this.CssClasses_.IS_ACTIVE);
+};
+/**
+ * Hide tooltip on mouseleave or scroll
+ *
+ * @private
+ */
+MaterialTooltip.prototype.hideTooltip_ = function () {
+ this.element_.classList.remove(this.CssClasses_.IS_ACTIVE);
+};
+/**
+ * Initialize element.
+ */
+MaterialTooltip.prototype.init = function () {
+ if (this.element_) {
+ var forElId = this.element_.getAttribute('for') || this.element_.getAttribute('data-mdl-for');
+ if (forElId) {
+ this.forElement_ = document.getElementById(forElId);
+ }
+ if (this.forElement_) {
+ // It's left here because it prevents accidental text selection on Android
+ if (!this.forElement_.hasAttribute('tabindex')) {
+ this.forElement_.setAttribute('tabindex', '0');
+ }
+ this.boundMouseEnterHandler = this.handleMouseEnter_.bind(this);
+ this.boundMouseLeaveAndScrollHandler = this.hideTooltip_.bind(this);
+ this.forElement_.addEventListener('mouseenter', this.boundMouseEnterHandler, false);
+ this.forElement_.addEventListener('touchend', this.boundMouseEnterHandler, false);
+ this.forElement_.addEventListener('mouseleave', this.boundMouseLeaveAndScrollHandler, false);
+ window.addEventListener('scroll', this.boundMouseLeaveAndScrollHandler, true);
+ window.addEventListener('touchstart', this.boundMouseLeaveAndScrollHandler);
+ }
+ }
+};
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialTooltip,
+ classAsString: 'MaterialTooltip',
+ cssClass: 'mdl-tooltip'
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Layout MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {HTMLElement} element The element that will be upgraded.
+ */
+var MaterialLayout = function MaterialLayout(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialLayout'] = MaterialLayout;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialLayout.prototype.Constant_ = {
+ MAX_WIDTH: '(max-width: 1024px)',
+ TAB_SCROLL_PIXELS: 100,
+ RESIZE_TIMEOUT: 100,
+ MENU_ICON: '',
+ CHEVRON_LEFT: 'chevron_left',
+ CHEVRON_RIGHT: 'chevron_right'
+};
+/**
+ * Keycodes, for code readability.
+ *
+ * @enum {number}
+ * @private
+ */
+MaterialLayout.prototype.Keycodes_ = {
+ ENTER: 13,
+ ESCAPE: 27,
+ SPACE: 32
+};
+/**
+ * Modes.
+ *
+ * @enum {number}
+ * @private
+ */
+MaterialLayout.prototype.Mode_ = {
+ STANDARD: 0,
+ SEAMED: 1,
+ WATERFALL: 2,
+ SCROLL: 3
+};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialLayout.prototype.CssClasses_ = {
+ CONTAINER: 'mdl-layout__container',
+ HEADER: 'mdl-layout__header',
+ DRAWER: 'mdl-layout__drawer',
+ CONTENT: 'mdl-layout__content',
+ DRAWER_BTN: 'mdl-layout__drawer-button',
+ ICON: 'material-icons',
+ JS_RIPPLE_EFFECT: 'mdl-js-ripple-effect',
+ RIPPLE_CONTAINER: 'mdl-layout__tab-ripple-container',
+ RIPPLE: 'mdl-ripple',
+ RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
+ HEADER_SEAMED: 'mdl-layout__header--seamed',
+ HEADER_WATERFALL: 'mdl-layout__header--waterfall',
+ HEADER_SCROLL: 'mdl-layout__header--scroll',
+ FIXED_HEADER: 'mdl-layout--fixed-header',
+ OBFUSCATOR: 'mdl-layout__obfuscator',
+ TAB_BAR: 'mdl-layout__tab-bar',
+ TAB_CONTAINER: 'mdl-layout__tab-bar-container',
+ TAB: 'mdl-layout__tab',
+ TAB_BAR_BUTTON: 'mdl-layout__tab-bar-button',
+ TAB_BAR_LEFT_BUTTON: 'mdl-layout__tab-bar-left-button',
+ TAB_BAR_RIGHT_BUTTON: 'mdl-layout__tab-bar-right-button',
+ PANEL: 'mdl-layout__tab-panel',
+ HAS_DRAWER: 'has-drawer',
+ HAS_TABS: 'has-tabs',
+ HAS_SCROLLING_HEADER: 'has-scrolling-header',
+ CASTING_SHADOW: 'is-casting-shadow',
+ IS_COMPACT: 'is-compact',
+ IS_SMALL_SCREEN: 'is-small-screen',
+ IS_DRAWER_OPEN: 'is-visible',
+ IS_ACTIVE: 'is-active',
+ IS_UPGRADED: 'is-upgraded',
+ IS_ANIMATING: 'is-animating',
+ ON_LARGE_SCREEN: 'mdl-layout--large-screen-only',
+ ON_SMALL_SCREEN: 'mdl-layout--small-screen-only'
+};
+/**
+ * Handles scrolling on the content.
+ *
+ * @private
+ */
+MaterialLayout.prototype.contentScrollHandler_ = function () {
+ if (this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)) {
+ return;
+ }
+ var headerVisible = !this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN) || this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);
+ if (this.content_.scrollTop > 0 && !this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
+ this.header_.classList.add(this.CssClasses_.CASTING_SHADOW);
+ this.header_.classList.add(this.CssClasses_.IS_COMPACT);
+ if (headerVisible) {
+ this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
+ }
+ } else if (this.content_.scrollTop <= 0 && this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
+ this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW);
+ this.header_.classList.remove(this.CssClasses_.IS_COMPACT);
+ if (headerVisible) {
+ this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
+ }
+ }
+};
+/**
+ * Handles a keyboard event on the drawer.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialLayout.prototype.keyboardEventHandler_ = function (evt) {
+ // Only react when the drawer is open.
+ if (evt.keyCode === this.Keycodes_.ESCAPE && this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)) {
+ this.toggleDrawer();
+ }
+};
+/**
+ * Handles changes in screen size.
+ *
+ * @private
+ */
+MaterialLayout.prototype.screenSizeHandler_ = function () {
+ if (this.screenSizeMediaQuery_.matches) {
+ this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN);
+ } else {
+ this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN);
+ // Collapse drawer (if any) when moving to a large screen size.
+ if (this.drawer_) {
+ this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN);
+ this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN);
+ }
+ }
+};
+/**
+ * Handles events of drawer button.
+ *
+ * @param {Event} evt The event that fired.
+ * @private
+ */
+MaterialLayout.prototype.drawerToggleHandler_ = function (evt) {
+ if (evt && evt.type === 'keydown') {
+ if (evt.keyCode === this.Keycodes_.SPACE || evt.keyCode === this.Keycodes_.ENTER) {
+ // prevent scrolling in drawer nav
+ evt.preventDefault();
+ } else {
+ // prevent other keys
+ return;
+ }
+ }
+ this.toggleDrawer();
+};
+/**
+ * Handles (un)setting the `is-animating` class
+ *
+ * @private
+ */
+MaterialLayout.prototype.headerTransitionEndHandler_ = function () {
+ this.header_.classList.remove(this.CssClasses_.IS_ANIMATING);
+};
+/**
+ * Handles expanding the header on click
+ *
+ * @private
+ */
+MaterialLayout.prototype.headerClickHandler_ = function () {
+ if (this.header_.classList.contains(this.CssClasses_.IS_COMPACT)) {
+ this.header_.classList.remove(this.CssClasses_.IS_COMPACT);
+ this.header_.classList.add(this.CssClasses_.IS_ANIMATING);
+ }
+};
+/**
+ * Reset tab state, dropping active classes
+ *
+ * @private
+ */
+MaterialLayout.prototype.resetTabState_ = function (tabBar) {
+ for (var k = 0; k < tabBar.length; k++) {
+ tabBar[k].classList.remove(this.CssClasses_.IS_ACTIVE);
+ }
+};
+/**
+ * Reset panel state, droping active classes
+ *
+ * @private
+ */
+MaterialLayout.prototype.resetPanelState_ = function (panels) {
+ for (var j = 0; j < panels.length; j++) {
+ panels[j].classList.remove(this.CssClasses_.IS_ACTIVE);
+ }
+};
+/**
+ * Toggle drawer state
+ *
+ * @public
+ */
+MaterialLayout.prototype.toggleDrawer = function () {
+ var drawerButton = this.element_.querySelector('.' + this.CssClasses_.DRAWER_BTN);
+ this.drawer_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN);
+ this.obfuscator_.classList.toggle(this.CssClasses_.IS_DRAWER_OPEN);
+ // Set accessibility properties.
+ if (this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)) {
+ this.drawer_.setAttribute('aria-hidden', 'false');
+ drawerButton.setAttribute('aria-expanded', 'true');
+ } else {
+ this.drawer_.setAttribute('aria-hidden', 'true');
+ drawerButton.setAttribute('aria-expanded', 'false');
+ }
+};
+MaterialLayout.prototype['toggleDrawer'] = MaterialLayout.prototype.toggleDrawer;
+/**
+ * Initialize element.
+ */
+MaterialLayout.prototype.init = function () {
+ if (this.element_) {
+ var container = document.createElement('div');
+ container.classList.add(this.CssClasses_.CONTAINER);
+ var focusedElement = this.element_.querySelector(':focus');
+ this.element_.parentElement.insertBefore(container, this.element_);
+ this.element_.parentElement.removeChild(this.element_);
+ container.appendChild(this.element_);
+ if (focusedElement) {
+ focusedElement.focus();
+ }
+ var directChildren = this.element_.childNodes;
+ var numChildren = directChildren.length;
+ for (var c = 0; c < numChildren; c++) {
+ var child = directChildren[c];
+ if (child.classList && child.classList.contains(this.CssClasses_.HEADER)) {
+ this.header_ = child;
+ }
+ if (child.classList && child.classList.contains(this.CssClasses_.DRAWER)) {
+ this.drawer_ = child;
+ }
+ if (child.classList && child.classList.contains(this.CssClasses_.CONTENT)) {
+ this.content_ = child;
+ }
+ }
+ window.addEventListener('pageshow', function (e) {
+ if (e.persisted) {
+ // when page is loaded from back/forward cache
+ // trigger repaint to let layout scroll in safari
+ this.element_.style.overflowY = 'hidden';
+ requestAnimationFrame(function () {
+ this.element_.style.overflowY = '';
+ }.bind(this));
+ }
+ }.bind(this), false);
+ if (this.header_) {
+ this.tabBar_ = this.header_.querySelector('.' + this.CssClasses_.TAB_BAR);
+ }
+ var mode = this.Mode_.STANDARD;
+ if (this.header_) {
+ if (this.header_.classList.contains(this.CssClasses_.HEADER_SEAMED)) {
+ mode = this.Mode_.SEAMED;
+ } else if (this.header_.classList.contains(this.CssClasses_.HEADER_WATERFALL)) {
+ mode = this.Mode_.WATERFALL;
+ this.header_.addEventListener('transitionend', this.headerTransitionEndHandler_.bind(this));
+ this.header_.addEventListener('click', this.headerClickHandler_.bind(this));
+ } else if (this.header_.classList.contains(this.CssClasses_.HEADER_SCROLL)) {
+ mode = this.Mode_.SCROLL;
+ container.classList.add(this.CssClasses_.HAS_SCROLLING_HEADER);
+ }
+ if (mode === this.Mode_.STANDARD) {
+ this.header_.classList.add(this.CssClasses_.CASTING_SHADOW);
+ if (this.tabBar_) {
+ this.tabBar_.classList.add(this.CssClasses_.CASTING_SHADOW);
+ }
+ } else if (mode === this.Mode_.SEAMED || mode === this.Mode_.SCROLL) {
+ this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW);
+ if (this.tabBar_) {
+ this.tabBar_.classList.remove(this.CssClasses_.CASTING_SHADOW);
+ }
+ } else if (mode === this.Mode_.WATERFALL) {
+ // Add and remove shadows depending on scroll position.
+ // Also add/remove auxiliary class for styling of the compact version of
+ // the header.
+ this.content_.addEventListener('scroll', this.contentScrollHandler_.bind(this));
+ this.contentScrollHandler_();
+ }
+ }
+ // Add drawer toggling button to our layout, if we have an openable drawer.
+ if (this.drawer_) {
+ var drawerButton = this.element_.querySelector('.' + this.CssClasses_.DRAWER_BTN);
+ if (!drawerButton) {
+ drawerButton = document.createElement('div');
+ drawerButton.setAttribute('aria-expanded', 'false');
+ drawerButton.setAttribute('role', 'button');
+ drawerButton.setAttribute('tabindex', '0');
+ drawerButton.classList.add(this.CssClasses_.DRAWER_BTN);
+ var drawerButtonIcon = document.createElement('i');
+ drawerButtonIcon.classList.add(this.CssClasses_.ICON);
+ drawerButtonIcon.innerHTML = this.Constant_.MENU_ICON;
+ drawerButton.appendChild(drawerButtonIcon);
+ }
+ if (this.drawer_.classList.contains(this.CssClasses_.ON_LARGE_SCREEN)) {
+ //If drawer has ON_LARGE_SCREEN class then add it to the drawer toggle button as well.
+ drawerButton.classList.add(this.CssClasses_.ON_LARGE_SCREEN);
+ } else if (this.drawer_.classList.contains(this.CssClasses_.ON_SMALL_SCREEN)) {
+ //If drawer has ON_SMALL_SCREEN class then add it to the drawer toggle button as well.
+ drawerButton.classList.add(this.CssClasses_.ON_SMALL_SCREEN);
+ }
+ drawerButton.addEventListener('click', this.drawerToggleHandler_.bind(this));
+ drawerButton.addEventListener('keydown', this.drawerToggleHandler_.bind(this));
+ // Add a class if the layout has a drawer, for altering the left padding.
+ // Adds the HAS_DRAWER to the elements since this.header_ may or may
+ // not be present.
+ this.element_.classList.add(this.CssClasses_.HAS_DRAWER);
+ // If we have a fixed header, add the button to the header rather than
+ // the layout.
+ if (this.element_.classList.contains(this.CssClasses_.FIXED_HEADER)) {
+ this.header_.insertBefore(drawerButton, this.header_.firstChild);
+ } else {
+ this.element_.insertBefore(drawerButton, this.content_);
+ }
+ var obfuscator = document.createElement('div');
+ obfuscator.classList.add(this.CssClasses_.OBFUSCATOR);
+ this.element_.appendChild(obfuscator);
+ obfuscator.addEventListener('click', this.drawerToggleHandler_.bind(this));
+ this.obfuscator_ = obfuscator;
+ this.drawer_.addEventListener('keydown', this.keyboardEventHandler_.bind(this));
+ this.drawer_.setAttribute('aria-hidden', 'true');
+ }
+ // Keep an eye on screen size, and add/remove auxiliary class for styling
+ // of small screens.
+ this.screenSizeMediaQuery_ = window.matchMedia(this.Constant_.MAX_WIDTH);
+ this.screenSizeMediaQuery_.addListener(this.screenSizeHandler_.bind(this));
+ this.screenSizeHandler_();
+ // Initialize tabs, if any.
+ if (this.header_ && this.tabBar_) {
+ this.element_.classList.add(this.CssClasses_.HAS_TABS);
+ var tabContainer = document.createElement('div');
+ tabContainer.classList.add(this.CssClasses_.TAB_CONTAINER);
+ this.header_.insertBefore(tabContainer, this.tabBar_);
+ this.header_.removeChild(this.tabBar_);
+ var leftButton = document.createElement('div');
+ leftButton.classList.add(this.CssClasses_.TAB_BAR_BUTTON);
+ leftButton.classList.add(this.CssClasses_.TAB_BAR_LEFT_BUTTON);
+ var leftButtonIcon = document.createElement('i');
+ leftButtonIcon.classList.add(this.CssClasses_.ICON);
+ leftButtonIcon.textContent = this.Constant_.CHEVRON_LEFT;
+ leftButton.appendChild(leftButtonIcon);
+ leftButton.addEventListener('click', function () {
+ this.tabBar_.scrollLeft -= this.Constant_.TAB_SCROLL_PIXELS;
+ }.bind(this));
+ var rightButton = document.createElement('div');
+ rightButton.classList.add(this.CssClasses_.TAB_BAR_BUTTON);
+ rightButton.classList.add(this.CssClasses_.TAB_BAR_RIGHT_BUTTON);
+ var rightButtonIcon = document.createElement('i');
+ rightButtonIcon.classList.add(this.CssClasses_.ICON);
+ rightButtonIcon.textContent = this.Constant_.CHEVRON_RIGHT;
+ rightButton.appendChild(rightButtonIcon);
+ rightButton.addEventListener('click', function () {
+ this.tabBar_.scrollLeft += this.Constant_.TAB_SCROLL_PIXELS;
+ }.bind(this));
+ tabContainer.appendChild(leftButton);
+ tabContainer.appendChild(this.tabBar_);
+ tabContainer.appendChild(rightButton);
+ // Add and remove tab buttons depending on scroll position and total
+ // window size.
+ var tabUpdateHandler = function () {
+ if (this.tabBar_.scrollLeft > 0) {
+ leftButton.classList.add(this.CssClasses_.IS_ACTIVE);
+ } else {
+ leftButton.classList.remove(this.CssClasses_.IS_ACTIVE);
+ }
+ if (this.tabBar_.scrollLeft < this.tabBar_.scrollWidth - this.tabBar_.offsetWidth) {
+ rightButton.classList.add(this.CssClasses_.IS_ACTIVE);
+ } else {
+ rightButton.classList.remove(this.CssClasses_.IS_ACTIVE);
+ }
+ }.bind(this);
+ this.tabBar_.addEventListener('scroll', tabUpdateHandler);
+ tabUpdateHandler();
+ // Update tabs when the window resizes.
+ var windowResizeHandler = function () {
+ // Use timeouts to make sure it doesn't happen too often.
+ if (this.resizeTimeoutId_) {
+ clearTimeout(this.resizeTimeoutId_);
+ }
+ this.resizeTimeoutId_ = setTimeout(function () {
+ tabUpdateHandler();
+ this.resizeTimeoutId_ = null;
+ }.bind(this), this.Constant_.RESIZE_TIMEOUT);
+ }.bind(this);
+ window.addEventListener('resize', windowResizeHandler);
+ if (this.tabBar_.classList.contains(this.CssClasses_.JS_RIPPLE_EFFECT)) {
+ this.tabBar_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
+ }
+ // Select element tabs, document panels
+ var tabs = this.tabBar_.querySelectorAll('.' + this.CssClasses_.TAB);
+ var panels = this.content_.querySelectorAll('.' + this.CssClasses_.PANEL);
+ // Create new tabs for each tab element
+ for (var i = 0; i < tabs.length; i++) {
+ new MaterialLayoutTab(tabs[i], tabs, panels, this);
+ }
+ }
+ this.element_.classList.add(this.CssClasses_.IS_UPGRADED);
+ }
+};
+/**
+ * Constructor for an individual tab.
+ *
+ * @constructor
+ * @param {HTMLElement} tab The HTML element for the tab.
+ * @param {!Array} tabs Array with HTML elements for all tabs.
+ * @param {!Array} panels Array with HTML elements for all panels.
+ * @param {MaterialLayout} layout The MaterialLayout object that owns the tab.
+ */
+function MaterialLayoutTab(tab, tabs, panels, layout) {
+ /**
+ * Auxiliary method to programmatically select a tab in the UI.
+ */
+ function selectTab() {
+ var href = tab.href.split('#')[1];
+ var panel = layout.content_.querySelector('#' + href);
+ layout.resetTabState_(tabs);
+ layout.resetPanelState_(panels);
+ tab.classList.add(layout.CssClasses_.IS_ACTIVE);
+ panel.classList.add(layout.CssClasses_.IS_ACTIVE);
+ }
+ if (layout.tabBar_.classList.contains(layout.CssClasses_.JS_RIPPLE_EFFECT)) {
+ var rippleContainer = document.createElement('span');
+ rippleContainer.classList.add(layout.CssClasses_.RIPPLE_CONTAINER);
+ rippleContainer.classList.add(layout.CssClasses_.JS_RIPPLE_EFFECT);
+ var ripple = document.createElement('span');
+ ripple.classList.add(layout.CssClasses_.RIPPLE);
+ rippleContainer.appendChild(ripple);
+ tab.appendChild(rippleContainer);
+ }
+ tab.addEventListener('click', function (e) {
+ if (tab.getAttribute('href').charAt(0) === '#') {
+ e.preventDefault();
+ selectTab();
+ }
+ });
+ tab.show = selectTab;
+}
+window['MaterialLayoutTab'] = MaterialLayoutTab;
+// The component registers itself. It can assume componentHandler is available
+// in the global scope.
+componentHandler.register({
+ constructor: MaterialLayout,
+ classAsString: 'MaterialLayout',
+ cssClass: 'mdl-js-layout'
+});
+/**
+ * @license
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Class constructor for Data Table Card MDL component.
+ * Implements MDL component design pattern defined at:
+ * https://github.com/jasonmayes/mdl-component-design-pattern
+ *
+ * @constructor
+ * @param {Element} element The element that will be upgraded.
+ */
+var MaterialDataTable = function MaterialDataTable(element) {
+ this.element_ = element;
+ // Initialize instance.
+ this.init();
+};
+window['MaterialDataTable'] = MaterialDataTable;
+/**
+ * Store constants in one place so they can be updated easily.
+ *
+ * @enum {string | number}
+ * @private
+ */
+MaterialDataTable.prototype.Constant_ = {};
+/**
+ * Store strings for class names defined by this component that are used in
+ * JavaScript. This allows us to simply change it in one place should we
+ * decide to modify at a later date.
+ *
+ * @enum {string}
+ * @private
+ */
+MaterialDataTable.prototype.CssClasses_ = {
+ DATA_TABLE: 'mdl-data-table',
+ SELECTABLE: 'mdl-data-table--selectable',
+ SELECT_ELEMENT: 'mdl-data-table__select',
+ IS_SELECTED: 'is-selected',
+ IS_UPGRADED: 'is-upgraded'
+};
+/**
+ * Generates and returns a function that toggles the selection state of a
+ * single row (or multiple rows).
+ *
+ * @param {Element} checkbox Checkbox that toggles the selection state.
+ * @param {Element} row Row to toggle when checkbox changes.
+ * @param {(Array