diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 57e2b1a1be..f9d7adceef 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -33,7 +33,6 @@
"allow_guests_to_upload_files",
"security",
"session_expiry",
- "session_expiry_mobile",
"document_share_key_expiry",
"column_break_13",
"deny_multiple_sessions",
@@ -211,13 +210,6 @@
"fieldtype": "Data",
"label": "Session Expiry (idle timeout)"
},
- {
- "default": "720:00",
- "description": "In Hours",
- "fieldname": "session_expiry_mobile",
- "fieldtype": "Data",
- "label": "Session Expiry Mobile"
- },
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
@@ -517,7 +509,7 @@
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2022-11-20 17:57:05.099512",
+ "modified": "2022-11-28 17:57:05.099512",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index 1fc27ca114..0f7a7f0819 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -20,11 +20,10 @@ class SystemSettings(Document):
elif not enable_password_policy:
self.minimum_password_score = ""
- for key in ("session_expiry", "session_expiry_mobile"):
- if self.get(key):
- parts = self.get(key).split(":")
- if len(parts) != 2 or not (cint(parts[0]) or cint(parts[1])):
- frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm"))
+ if self.session_expiry:
+ parts = self.session_expiry.split(":")
+ if len(parts) != 2 or not (cint(parts[0]) or cint(parts[1])):
+ frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm"))
if self.enable_two_factor_auth:
if self.two_factor_method == "SMS":
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 70b37dfcf8..efeeaaf935 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -252,7 +252,6 @@ CREATE TABLE `tabSessions` (
`sessiondata` longtext,
`ipaddress` varchar(16) DEFAULT NULL,
`lastupdate` datetime(6) DEFAULT NULL,
- `device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index ec31923f93..67f809abf7 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -135,9 +135,9 @@ def check_compatible_versions():
version = get_mariadb_version()
version_tuple = tuple(int(v) for v in version[0].split("."))
- if version_tuple < (10, 3):
+ if version_tuple < (10, 6):
click.secho(
- f"Warning: MariaDB version {version} is less than 10.3 which is not supported by Frappe",
+ f"Warning: MariaDB version {version} is less than 10.6 which is not supported by Frappe",
fg="yellow",
)
elif version_tuple >= (10, 9):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 7ce3cecff8..37605be0f6 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -256,7 +256,6 @@ CREATE TABLE "tabSessions" (
"sessiondata" text,
"ipaddress" varchar(16) DEFAULT NULL,
"lastupdate" timestamp(6) DEFAULT NULL,
- "device" varchar(255) DEFAULT 'desktop',
"status" varchar(20) DEFAULT NULL
);
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index ee975c8326..b5b58ebfa3 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -156,6 +156,9 @@ class FormMeta(Meta):
list_script = ""
form_script = ""
for script in client_scripts:
+ if not script.script:
+ continue
+
if script.view == "List":
list_script += f"""
// {script.name}
@@ -163,7 +166,7 @@ class FormMeta(Meta):
"""
- if script.view == "Form":
+ elif script.view == "Form":
form_script += f"""
// {script.name}
{script.script}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index a1e8a9368f..b4a51ffaf3 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -319,7 +319,7 @@ def format_duration_fields(data: frappe._dict) -> None:
continue
for row in data.result:
- index = col.fieldname if isinstance(row, dict) else i
+ index = col.get("fieldname") if isinstance(row, dict) else i
if row[index]:
row[index] = format_duration(row[index])
diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py
index 5738a1837d..8b1ec1364f 100644
--- a/frappe/geo/doctype/country/country.py
+++ b/frappe/geo/doctype/country/country.py
@@ -29,6 +29,8 @@ def get_countries_and_currencies():
countries = []
currencies = []
+ added_currencies = set()
+
for name, country in data.items():
country = frappe._dict(country)
countries.append(
@@ -42,7 +44,9 @@ def get_countries_and_currencies():
time_zones="\n".join(country.timezones or []),
)
)
- if country.currency:
+ if country.currency and country.currency not in added_currencies:
+ added_currencies.add(country.currency)
+
currencies.append(
frappe.get_doc(
doctype="Currency",
diff --git a/frappe/model/document.py b/frappe/model/document.py
index b81aa79cde..e6f90b2260 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -708,17 +708,16 @@ class Document(BaseDocument):
d.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)
def get_permlevel_access(self, permission_type="write"):
- if not hasattr(self, "_has_access_to"):
- self._has_access_to = {}
-
- self._has_access_to[permission_type] = []
+ allowed_permlevels = []
roles = frappe.get_roles()
- for perm in self.get_permissions():
- if perm.role in roles and perm.get(permission_type):
- if perm.permlevel not in self._has_access_to[permission_type]:
- self._has_access_to[permission_type].append(perm.permlevel)
- return self._has_access_to[permission_type]
+ for perm in self.get_permissions():
+ if (
+ perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels
+ ):
+ allowed_permlevels.append(perm.permlevel)
+
+ return allowed_permlevels
def has_permlevel_access_to(self, fieldname, df=None, permission_type="read"):
if not df:
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 99553f470c..146676652e 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -644,6 +644,8 @@ $.extend(frappe.model, {
doctype: doctype,
name: docname,
},
+ freeze: true,
+ freeze_message: __("Deleting {0}...", [title]),
callback: function (r, rt) {
if (!r.exc) {
frappe.utils.play_sound("delete");
diff --git a/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js
index 765377a0b4..9f86b03744 100644
--- a/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js
+++ b/frappe/public/js/frappe/ui/toolbar/fuzzy_match.js
@@ -14,7 +14,7 @@
// J�rgen Tjern� - async helper
// Anurag Awasthi - updated to 0.2.0
-const SEQUENTIAL_BONUS = 15; // bonus for adjacent matches
+const SEQUENTIAL_BONUS = 25; // bonus for adjacent matches
const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator
const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower
const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched
diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js
index 52d417f025..980df7129b 100644
--- a/frappe/public/js/frappe/web_form/web_form.js
+++ b/frappe/public/js/frappe/web_form/web_form.js
@@ -379,7 +379,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
args: {
data: this.doc,
web_form: this.name,
- docname: this.doc.name,
for_payment,
},
callback: (response) => {
diff --git a/frappe/query_builder/docs.md b/frappe/query_builder/docs.md
new file mode 100644
index 0000000000..c1f170b3d4
--- /dev/null
+++ b/frappe/query_builder/docs.md
@@ -0,0 +1,117 @@
+# This documentation is added for query builder and related files.
+
+## Related Files
+
+- [builder](./builder.py)
+- [custom](./custom.py)
+- [functions](./functions.py)
+- [terms](./terms.py)
+- [query](../database/query.py)
+
+### Builder
+
+Database specefic classes are declared which are then selected during init to give either postgres or mariadb dialects.
+
+### Functions and Custom
+
+These file handle any custom function which needs to be either added or handled sperately by the different dialects which are not supported yet by pypika directly.
+
+### Terms
+
+The inherent terms or specefic classes of pypika builder are handled and declared here all the parameterization goes through this module (custom parameterization is also implemeted here).
+
+### Raw Query Generation Examples
+
+Check out some examples [here](https://frappeframework.com/docs/v14/user/en/api/query-builder)
+
+
Query
+
+## Goal
+
+```sql
+select `name` from `tabUser`
+```
+
+## There are 3 major ways to reach this goal
+
+### 1. Direct SQL (Boring / Unsafe / inconsistent)
+
+```python
+frappe.db.sql("select `name` from `tabUser`")
+```
+
+### 2. SQL through direct Query Builder objects
+
+```python
+from frappe.query_builder import Field
+
+frappe.qb.from_("User").select(Field("name"))
+
+```
+
+### 3. Through the database API (Which performs the second method under the hood)
+
+```python
+frappe.db.get_values("User", fieldname="name", filters={})
+```
+
+This module is used to support the 3rd way of query generation in frappe.
+The database module is completely powered by this query module.
+This module is also where the query `Engine` resides which is the class responsible for the handling of various filter & field notations.
+
+- Interating with the existing Database API.
+ - The old database API was running on raw sql generation in order to bridge the gap between the added new support and raw sql strings this intermediate module was added.
+
+This module supports almost all the features present in db_query which powers `frappe.get_all` and `frappe.get_list`
+
+Supporting all the features with the previous filter notations and the field notations few features were added -:
+
+1. Dict Query
+
+ - To support this
+
+ ```python
+ frappe.db.get_values("ToDo", fieldname="name", filters={"description": "Something Random"})
+ ```
+
+ and many other possible caveats to the dict representation such as
+
+ ```python
+ frappe.db.get_values("User", fieldname="name",
+ filters={"name": ("like", "admin%")})
+
+ frappe.db.get_values("ToDo", fieldname="name", filters={"description": ("in", ["somso%", "someome"])})
+ ```
+
+2. Misc Query
+
+ - To support this
+
+ ```python
+ frappe.db.get_values("ToDo", fieldname="name", filters=["description", "=", "someone"])
+ ```
+
+ Along with other possible list filter use cases including implicit joins
+
+3. Criterion Query
+
+ - To support Inherent Query Builder objects
+
+ ```python
+ from frappe.query_builder import Field
+
+ frappe.db.get_values("User", fieldname="name", filters=Field("name") == "Administrator")
+
+ ```
+
+ and all the pypika filters and functions.
+
+## Things to be implemented in the `Engine`
+
+### 1. Support for Permissions
+
+As of now query builder has no concept of permissions and moving towards a singular database API this needs to be added in the `Engine`.
+
+### 2. Implementing the missing features which are present in `frappe.get_list` and `frappe.get_all` (do we even need so much magic?)
+
+Moving to a singular Database API (database.py + db_query.py) all the support present in `get_list` and `get_all` needs to be present in the new `Engine` as well however this creates alot of security cracks, so moving the a *new and more restrictive version* of the database API with backward compatibility perhaps would be the right way to go?
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 20891db2e6..9c739f3a96 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -32,12 +32,11 @@ def clear():
frappe.response["message"] = _("Cache Cleared")
-def clear_sessions(user=None, keep_current=False, device=None, force=False):
+def clear_sessions(user=None, keep_current=False, force=False):
"""Clear other sessions of the current user. Called at login / logout
:param user: user name (default: current user)
:param keep_current: keep current session (default: false)
- :param device: delete sessions of this device (default: desktop, mobile)
:param force: triggered by the user (default false)
"""
@@ -45,35 +44,26 @@ def clear_sessions(user=None, keep_current=False, device=None, force=False):
if force:
reason = "Force Logged out by the user"
- for sid in get_sessions_to_clear(user, keep_current, device):
+ for sid in get_sessions_to_clear(user, keep_current):
delete_session(sid, reason=reason)
-def get_sessions_to_clear(user=None, keep_current=False, device=None):
+def get_sessions_to_clear(user=None, keep_current=False):
"""Returns sessions of the current user. Called at login / logout
:param user: user name (default: current user)
:param keep_current: keep current session (default: false)
- :param device: delete sessions of this device (default: desktop, mobile)
"""
if not user:
user = frappe.session.user
- if not device:
- device = ("desktop", "mobile")
-
- if not isinstance(device, (tuple, list)):
- device = (device,)
-
offset = 0
if user == frappe.session.user:
simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1
offset = simultaneous_sessions - 1
session = DocType("Sessions")
- session_id = frappe.qb.from_(session).where(
- (session.user == user) & (session.device.isin(device))
- )
+ session_id = frappe.qb.from_(session).where(session.user == user)
if keep_current:
session_id = session_id.where(session.sid != frappe.session.sid)
@@ -121,25 +111,18 @@ def clear_all_sessions(reason=None):
def get_expired_sessions():
"""Returns list of expired sessions"""
+
sessions = DocType("Sessions")
- expired = []
- for device in ("desktop", "mobile"):
- expired.extend(
- frappe.db.get_values(
- sessions,
- filters=(
- PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})")
- > get_expiry_period_for_query(device)
- )
- & (sessions.device == device),
- fieldname="sid",
- order_by=None,
- pluck=True,
- )
- )
-
- return expired
+ return frappe.db.get_values(
+ sessions,
+ filters=(
+ PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") > get_expiry_period_for_query()
+ ),
+ fieldname="sid",
+ order_by=None,
+ pluck=True,
+ )
def clear_expired_sessions():
@@ -218,14 +201,13 @@ def generate_csrf_token():
class Session:
- __slots__ = ("user", "device", "user_type", "full_name", "data", "time_diff", "sid")
+ __slots__ = ("user", "user_type", "full_name", "data", "time_diff", "sid")
def __init__(self, user, resume=False, full_name=None, user_type=None):
self.sid = cstr(
frappe.form_dict.get("sid") or unquote(frappe.request.cookies.get("sid", "Guest"))
)
self.user = user
- self.device = frappe.form_dict.get("device") or "desktop"
self.user_type = user_type
self.full_name = full_name
self.data = frappe._dict({"data": frappe._dict({})})
@@ -257,10 +239,9 @@ class Session:
self.data.data.update(
{
"last_updated": frappe.utils.now(),
- "session_expiry": get_expiry_period(self.device),
+ "session_expiry": get_expiry_period(),
"full_name": self.full_name,
"user_type": self.user_type,
- "device": self.device,
"session_country": get_geo_ip_country(frappe.local.request_ip)
if frappe.local.request_ip
else None,
@@ -289,9 +270,9 @@ class Session:
def insert_session_record(self):
frappe.db.sql(
"""insert into `tabSessions`
- (`sessiondata`, `user`, `lastupdate`, `sid`, `status`, `device`)
- values (%s , %s, NOW(), %s, 'Active', %s)""",
- (str(self.data["data"]), self.data["user"], self.data["sid"], self.device),
+ (`sessiondata`, `user`, `lastupdate`, `sid`, `status`)
+ values (%s , %s, NOW(), %s, 'Active')""",
+ (str(self.data["data"]), self.data["user"], self.data["sid"]),
)
# also add to memcache
@@ -308,7 +289,6 @@ class Session:
self.data.update({"data": data, "user": data.user, "sid": self.sid})
self.user = data.user
validate_ip_address(self.user)
- self.device = data.device
else:
self.start_as_guest()
@@ -359,22 +339,11 @@ class Session:
def get_session_data_from_db(self):
sessions = DocType("Sessions")
-
- self.device = (
- frappe.db.get_value(
- sessions,
- filters=sessions.sid == self.sid,
- fieldname="device",
- order_by=None,
- )
- or "desktop"
- )
rec = frappe.db.get_values(
sessions,
filters=(sessions.sid == self.sid)
& (
- PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})")
- < get_expiry_period_for_query(self.device)
+ PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})") < get_expiry_period_for_query()
),
fieldname=["user", "sessiondata"],
order_by=None,
@@ -437,29 +406,23 @@ class Session:
return updated_in_db
-def get_expiry_period_for_query(device=None):
+def get_expiry_period_for_query():
if frappe.db.db_type == "postgres":
- return get_expiry_period(device)
+ return get_expiry_period()
else:
- return get_expiry_in_seconds(device=device)
+ return get_expiry_in_seconds()
-def get_expiry_in_seconds(expiry=None, device=None):
+def get_expiry_in_seconds(expiry=None):
if not expiry:
- expiry = get_expiry_period(device)
+ expiry = get_expiry_period()
+
parts = expiry.split(":")
return (cint(parts[0]) * 3600) + (cint(parts[1]) * 60) + cint(parts[2])
-def get_expiry_period(device="desktop"):
- if device == "mobile":
- key = "session_expiry_mobile"
- default = "720:00:00"
- else:
- key = "session_expiry"
- default = "06:00:00"
-
- exp_sec = frappe.defaults.get_global_default(key) or default
+def get_expiry_period():
+ exp_sec = frappe.defaults.get_global_default("session_expiry") or "06:00:00"
# incase seconds is missing
if len(exp_sec.split(":")) == 2:
diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js
index 8365947e6c..60dd0396de 100644
--- a/frappe/templates/includes/login/login.js
+++ b/frappe/templates/includes/login/login.js
@@ -19,7 +19,6 @@ login.bind_events = function () {
args.cmd = "login";
args.usr = frappe.utils.xss_sanitise(($("#login_email").val() || "").trim());
args.pwd = $("#login_password").val();
- args.device = "desktop";
if (!args.usr || !args.pwd) {
frappe.msgprint('{{ _("Both login and password required") }}');
return false;
@@ -73,7 +72,6 @@ login.bind_events = function () {
args.cmd = "{{ ldap_settings.method }}";
args.usr = ($("#login_email").val() || "").trim();
args.pwd = $("#login_password").val();
- args.device = "desktop";
if (!args.usr || !args.pwd) {
login.set_status('{{ _("Both login and password required") }}', 'red');
return false;
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index 0d5abd52e8..cd15c957f9 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -169,6 +169,7 @@ def before_tests():
if not int(frappe.db.get_single_value("System Settings", "setup_complete") or 0):
complete_setup_wizard()
+ frappe.db.set_value("Website Settings", "Website Settings", "disable_signup", 0)
frappe.db.commit()
frappe.clear_cache()
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index d102ac2fd8..73b2dc2331 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -342,7 +342,7 @@ def get_context(context):
return False
if self.apply_document_permissions:
- return frappe.get_doc(doctype, name).has_permission()
+ return frappe.get_doc(doctype, name).has_permission(permtype=ptype)
# owner matches
elif frappe.db.get_value(doctype, name, "owner") == frappe.session.user:
@@ -365,7 +365,7 @@ def get_web_form_module(doc):
@frappe.whitelist(allow_guest=True)
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"])
-def accept(web_form, data, docname=None):
+def accept(web_form, data):
"""Save the web form"""
data = frappe._dict(json.loads(data))
@@ -373,19 +373,20 @@ def accept(web_form, data, docname=None):
files_to_delete = []
web_form = frappe.get_doc("Web Form", web_form)
+ doctype = web_form.doc_type
if data.name and not web_form.allow_edit:
frappe.throw(_("You are not allowed to update this Web Form Document"))
frappe.flags.in_web_form = True
- meta = frappe.get_meta(data.doctype)
+ meta = frappe.get_meta(doctype)
- if docname:
+ if data.name:
# update
- doc = frappe.get_doc(data.doctype, docname)
+ doc = frappe.get_doc(doctype, data.name)
else:
# insert
- doc = frappe.new_doc(data.doctype)
+ doc = frappe.new_doc(doctype)
# set values
for field in web_form.web_form_fields:
@@ -406,7 +407,7 @@ def accept(web_form, data, docname=None):
doc.set(fieldname, value)
if doc.name:
- if web_form.has_web_form_permission(doc.doctype, doc.name, "write"):
+ if web_form.has_web_form_permission(doctype, doc.name, "write"):
doc.save(ignore_permissions=True)
else:
# only if permissions are present
@@ -428,7 +429,7 @@ def accept(web_form, data, docname=None):
# remove earlier attached file (if exists)
if doc.get(fieldname):
- remove_file_by_url(doc.get(fieldname), doctype=doc.doctype, name=doc.name)
+ remove_file_by_url(doc.get(fieldname), doctype=doctype, name=doc.name)
# save new file
filename, dataurl = filedata.split(",", 1)
@@ -436,7 +437,7 @@ def accept(web_form, data, docname=None):
{
"doctype": "File",
"file_name": filename,
- "attached_to_doctype": doc.doctype,
+ "attached_to_doctype": doctype,
"attached_to_name": doc.name,
"content": dataurl,
"decode": True,
@@ -452,7 +453,7 @@ def accept(web_form, data, docname=None):
if files_to_delete:
for f in files_to_delete:
if f:
- remove_file_by_url(f, doctype=doc.doctype, name=doc.name)
+ remove_file_by_url(f, doctype=doctype, name=doc.name)
frappe.flags.web_form_doc = doc
return doc
diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json
index 29827afd80..3bf7e4cb30 100644
--- a/frappe/website/doctype/website_settings/website_settings.json
+++ b/frappe/website/doctype/website_settings/website_settings.json
@@ -247,7 +247,7 @@
"read_only": 1
},
{
- "default": "0",
+ "default": "1",
"description": "Disable Customer Signup link in Login page",
"fieldname": "disable_signup",
"fieldtype": "Check",
@@ -476,7 +476,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-10-18 09:50:24.621839",
+ "modified": "2022-12-05 04:17:56.478757",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",