2020-06-11 00:54:34 +02:00
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import re
|
2016-07-29 15:06:41 +02:00
|
|
|
from collections import OrderedDict
|
2021-06-03 18:10:28 +02:00
|
|
|
from dataclasses import dataclass
|
2024-04-16 18:27:55 +02:00
|
|
|
from typing import Any, Callable, Dict, List, Optional
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2016-08-14 03:32:11 +02:00
|
|
|
from django.conf import settings
|
2016-11-09 01:45:36 +01:00
|
|
|
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
|
2017-04-03 11:49:13 +02:00
|
|
|
from django.template import loader
|
2024-04-16 18:27:55 +02:00
|
|
|
from django.template.response import TemplateResponse
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.views.generic import TemplateView
|
2020-07-14 02:53:18 +02:00
|
|
|
from lxml import html
|
|
|
|
from lxml.etree import Element, SubElement, XPath, _Element
|
|
|
|
from markupsafe import Markup
|
2023-10-12 19:43:45 +02:00
|
|
|
from typing_extensions import override
|
2016-07-29 15:06:41 +02:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.context_processors import zulip_default_context
|
2020-05-08 06:37:58 +02:00
|
|
|
from zerver.decorator import add_google_analytics_context
|
2024-04-16 18:27:55 +02:00
|
|
|
from zerver.lib.html_to_text import get_content_description
|
2022-09-08 13:36:05 +02:00
|
|
|
from zerver.lib.integrations import (
|
|
|
|
CATEGORIES,
|
|
|
|
INTEGRATIONS,
|
|
|
|
META_CATEGORY,
|
|
|
|
HubotIntegration,
|
|
|
|
WebhookIntegration,
|
2023-08-07 12:25:33 +02:00
|
|
|
get_all_event_types_for_integration,
|
2022-09-08 13:36:05 +02:00
|
|
|
)
|
2024-04-16 18:27:55 +02:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2017-10-19 07:21:57 +02:00
|
|
|
from zerver.lib.subdomains import get_subdomain
|
2021-06-11 10:45:10 +02:00
|
|
|
from zerver.lib.templates import render_markdown_path
|
2017-10-20 02:56:49 +02:00
|
|
|
from zerver.models import Realm
|
2021-05-23 09:46:10 +02:00
|
|
|
from zerver.openapi.openapi import get_endpoint_from_operationid, get_openapi_summary
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2016-07-29 15:06:41 +02:00
|
|
|
|
2021-06-03 18:10:28 +02:00
|
|
|
@dataclass
|
|
|
|
class DocumentationArticle:
|
|
|
|
article_path: str
|
|
|
|
article_http_status: int
|
|
|
|
endpoint_path: Optional[str]
|
|
|
|
endpoint_method: Optional[str]
|
|
|
|
|
|
|
|
|
2023-04-26 02:28:23 +02:00
|
|
|
def add_api_url_context(context: Dict[str, Any], request: HttpRequest) -> None:
|
2018-06-01 20:31:16 +02:00
|
|
|
context.update(zulip_default_context(request))
|
|
|
|
|
2017-10-02 07:59:20 +02:00
|
|
|
subdomain = get_subdomain(request)
|
2021-02-12 08:19:30 +01:00
|
|
|
if subdomain != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN or not settings.ROOT_DOMAIN_LANDING_PAGE:
|
2017-10-02 07:59:20 +02:00
|
|
|
display_subdomain = subdomain
|
|
|
|
html_settings_links = True
|
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
display_subdomain = "yourZulipDomain"
|
2017-10-02 07:59:20 +02:00
|
|
|
html_settings_links = False
|
2016-07-19 14:35:08 +02:00
|
|
|
|
2017-10-30 22:13:13 +01:00
|
|
|
display_host = Realm.host_for_subdomain(display_subdomain)
|
|
|
|
api_url_scheme_relative = display_host + "/api"
|
|
|
|
api_url = settings.EXTERNAL_URI_SCHEME + api_url_scheme_relative
|
2019-03-30 23:04:52 +01:00
|
|
|
zulip_url = settings.EXTERNAL_URI_SCHEME + display_host
|
2016-09-14 07:07:21 +02:00
|
|
|
|
2023-04-26 02:24:11 +02:00
|
|
|
context["external_url_scheme"] = settings.EXTERNAL_URI_SCHEME
|
2021-02-12 08:20:45 +01:00
|
|
|
context["api_url"] = api_url
|
|
|
|
context["api_url_scheme_relative"] = api_url_scheme_relative
|
|
|
|
context["zulip_url"] = zulip_url
|
2018-04-05 20:31:43 +02:00
|
|
|
|
2016-07-19 14:35:08 +02:00
|
|
|
context["html_settings_links"] = html_settings_links
|
2016-09-14 07:07:21 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2016-09-13 19:09:03 +02:00
|
|
|
class ApiURLView(TemplateView):
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2017-11-27 09:28:57 +01:00
|
|
|
def get_context_data(self, **kwargs: Any) -> Dict[str, str]:
|
2017-10-27 08:28:23 +02:00
|
|
|
context = super().get_context_data(**kwargs)
|
2023-04-26 02:28:23 +02:00
|
|
|
add_api_url_context(context, self.request)
|
2016-09-13 19:09:03 +02:00
|
|
|
return context
|
|
|
|
|
2016-08-14 09:44:12 +02:00
|
|
|
|
2023-08-21 23:55:47 +02:00
|
|
|
sidebar_headings = XPath("//*[self::h1 or self::h2 or self::h3 or self::h4]")
|
2020-07-14 02:53:18 +02:00
|
|
|
sidebar_links = XPath("//a[@href=$url]")
|
|
|
|
|
|
|
|
|
2017-07-25 02:35:22 +02:00
|
|
|
class MarkdownDirectoryView(ApiURLView):
|
|
|
|
path_template = ""
|
2021-11-03 21:36:54 +01:00
|
|
|
policies_view = False
|
2023-01-25 23:08:29 +01:00
|
|
|
help_view = False
|
2023-01-31 12:11:45 +01:00
|
|
|
api_doc_view = False
|
2016-11-09 01:45:36 +01:00
|
|
|
|
2024-04-16 18:27:55 +02:00
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self._post_render_callbacks: List[Callable[[HttpResponse], Optional[HttpResponse]]] = []
|
|
|
|
|
|
|
|
def add_post_render_callback(
|
|
|
|
self, callback: Callable[[HttpResponse], Optional[HttpResponse]]
|
|
|
|
) -> None:
|
|
|
|
self._post_render_callbacks.append(callback)
|
|
|
|
|
2021-06-03 18:17:57 +02:00
|
|
|
def get_path(self, article: str) -> DocumentationArticle:
|
2018-12-03 23:03:19 +01:00
|
|
|
http_status = 200
|
2016-11-09 01:45:36 +01:00
|
|
|
if article == "":
|
|
|
|
article = "index"
|
2018-08-18 15:42:01 +02:00
|
|
|
elif article == "include/sidebar_index":
|
|
|
|
pass
|
2023-01-31 17:33:25 +01:00
|
|
|
elif article == "api-doc-template":
|
|
|
|
# This markdown template shouldn't be accessed directly.
|
|
|
|
article = "missing"
|
|
|
|
http_status = 404
|
2017-10-02 19:51:17 +02:00
|
|
|
elif "/" in article:
|
|
|
|
article = "missing"
|
2018-12-03 23:03:19 +01:00
|
|
|
http_status = 404
|
2024-04-26 20:30:22 +02:00
|
|
|
elif len(article) > 100 or not re.match(r"^[0-9a-zA-Z_-]+$", article):
|
2018-12-03 23:03:19 +01:00
|
|
|
article = "missing"
|
|
|
|
http_status = 404
|
2018-12-06 19:11:02 +01:00
|
|
|
|
2018-12-03 23:03:19 +01:00
|
|
|
path = self.path_template % (article,)
|
2021-05-23 13:27:41 +02:00
|
|
|
endpoint_name = None
|
|
|
|
endpoint_method = None
|
|
|
|
|
2023-01-31 12:11:45 +01:00
|
|
|
if not self.path_template.startswith("/"):
|
|
|
|
# Relative paths only used for policies documentation
|
|
|
|
# when it is not configured or in the dev environment
|
|
|
|
assert self.policies_view
|
|
|
|
|
|
|
|
try:
|
|
|
|
loader.get_template(path)
|
|
|
|
return DocumentationArticle(
|
|
|
|
article_path=path,
|
|
|
|
article_http_status=http_status,
|
|
|
|
endpoint_path=endpoint_name,
|
|
|
|
endpoint_method=endpoint_method,
|
|
|
|
)
|
|
|
|
except loader.TemplateDoesNotExist:
|
|
|
|
return DocumentationArticle(
|
|
|
|
article_path=self.path_template % ("missing",),
|
|
|
|
article_http_status=404,
|
|
|
|
endpoint_path=None,
|
|
|
|
endpoint_method=None,
|
|
|
|
)
|
2021-11-03 21:36:54 +01:00
|
|
|
|
2023-01-31 12:11:45 +01:00
|
|
|
if not os.path.exists(path):
|
|
|
|
if self.api_doc_view:
|
2023-01-09 19:29:59 +01:00
|
|
|
try:
|
2023-01-31 12:11:45 +01:00
|
|
|
# API endpoints documented in zerver/openapi/zulip.yaml
|
2023-01-09 19:29:59 +01:00
|
|
|
endpoint_name, endpoint_method = get_endpoint_from_operationid(article)
|
2023-01-31 12:11:45 +01:00
|
|
|
path = self.path_template % ("api-doc-template",)
|
2023-01-09 19:29:59 +01:00
|
|
|
except AssertionError:
|
|
|
|
return DocumentationArticle(
|
|
|
|
article_path=self.path_template % ("missing",),
|
|
|
|
article_http_status=404,
|
|
|
|
endpoint_path=None,
|
|
|
|
endpoint_method=None,
|
|
|
|
)
|
2023-01-31 12:11:45 +01:00
|
|
|
elif self.help_view or self.policies_view:
|
|
|
|
article = "missing"
|
|
|
|
http_status = 404
|
|
|
|
path = self.path_template % (article,)
|
|
|
|
else:
|
|
|
|
raise AssertionError("Invalid documentation view type")
|
2021-11-03 21:36:54 +01:00
|
|
|
|
2023-01-31 12:11:45 +01:00
|
|
|
return DocumentationArticle(
|
|
|
|
article_path=path,
|
|
|
|
article_http_status=http_status,
|
|
|
|
endpoint_path=endpoint_name,
|
|
|
|
endpoint_method=endpoint_method,
|
|
|
|
)
|
2016-11-09 01:45:36 +01:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2017-11-27 09:28:57 +01:00
|
|
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
2016-11-09 01:45:36 +01:00
|
|
|
article = kwargs["article"]
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
context: Dict[str, Any] = super().get_context_data()
|
2021-06-03 18:17:57 +02:00
|
|
|
|
|
|
|
documentation_article = self.get_path(article)
|
|
|
|
context["article"] = documentation_article.article_path
|
2023-01-18 19:13:33 +01:00
|
|
|
not_index_page = not context["article"].endswith("/index.md")
|
|
|
|
|
2021-11-03 21:36:54 +01:00
|
|
|
if documentation_article.article_path.startswith("/") and os.path.exists(
|
|
|
|
documentation_article.article_path
|
|
|
|
):
|
|
|
|
# Absolute path case
|
2023-01-18 19:19:54 +01:00
|
|
|
article_absolute_path = documentation_article.article_path
|
2021-11-03 21:36:54 +01:00
|
|
|
else:
|
2023-01-31 12:11:45 +01:00
|
|
|
# Relative path case
|
2023-01-18 19:19:54 +01:00
|
|
|
article_absolute_path = os.path.join(
|
2021-11-03 21:36:54 +01:00
|
|
|
settings.DEPLOY_ROOT, "templates", documentation_article.article_path
|
|
|
|
)
|
2017-04-03 11:49:13 +02:00
|
|
|
|
2023-01-25 23:08:29 +01:00
|
|
|
if self.help_view:
|
2017-11-08 22:08:23 +01:00
|
|
|
context["page_is_help_center"] = True
|
2018-08-18 15:42:01 +02:00
|
|
|
context["doc_root"] = "/help/"
|
2021-06-29 18:10:16 +02:00
|
|
|
context["doc_root_title"] = "Help center"
|
2021-06-03 18:17:57 +02:00
|
|
|
sidebar_article = self.get_path("include/sidebar_index")
|
|
|
|
sidebar_index = sidebar_article.article_path
|
2022-09-08 13:36:05 +02:00
|
|
|
title_base = "Zulip help center"
|
2023-01-18 19:22:59 +01:00
|
|
|
elif self.policies_view:
|
2021-11-03 21:36:54 +01:00
|
|
|
context["page_is_policy_center"] = True
|
|
|
|
context["doc_root"] = "/policies/"
|
|
|
|
context["doc_root_title"] = "Terms and policies"
|
|
|
|
sidebar_article = self.get_path("sidebar_index")
|
2023-12-18 21:57:47 +01:00
|
|
|
if sidebar_article.article_http_status == 200:
|
|
|
|
sidebar_index = sidebar_article.article_path
|
|
|
|
else:
|
|
|
|
sidebar_index = None
|
2021-11-03 21:36:54 +01:00
|
|
|
title_base = "Zulip terms and policies"
|
2023-01-31 12:11:45 +01:00
|
|
|
elif self.api_doc_view:
|
2017-11-08 22:08:23 +01:00
|
|
|
context["page_is_api_center"] = True
|
2018-08-18 15:42:01 +02:00
|
|
|
context["doc_root"] = "/api/"
|
2021-06-29 18:10:16 +02:00
|
|
|
context["doc_root_title"] = "API documentation"
|
2021-06-03 18:17:57 +02:00
|
|
|
sidebar_article = self.get_path("sidebar_index")
|
|
|
|
sidebar_index = sidebar_article.article_path
|
2020-10-23 02:43:28 +02:00
|
|
|
title_base = "Zulip API documentation"
|
2023-01-31 12:11:45 +01:00
|
|
|
else:
|
|
|
|
raise AssertionError("Invalid documentation view type")
|
2018-12-14 23:28:00 +01:00
|
|
|
|
|
|
|
# The following is a somewhat hacky approach to extract titles from articles.
|
2021-12-07 03:29:47 +01:00
|
|
|
endpoint_name = None
|
|
|
|
endpoint_method = None
|
2023-01-18 19:19:54 +01:00
|
|
|
if os.path.exists(article_absolute_path):
|
|
|
|
with open(article_absolute_path) as article_file:
|
2018-12-14 23:28:00 +01:00
|
|
|
first_line = article_file.readlines()[0]
|
2023-01-31 12:11:45 +01:00
|
|
|
if self.api_doc_view and context["article"].endswith("api-doc-template.md"):
|
2021-05-23 13:27:41 +02:00
|
|
|
endpoint_name, endpoint_method = (
|
|
|
|
documentation_article.endpoint_path,
|
|
|
|
documentation_article.endpoint_method,
|
|
|
|
)
|
2021-07-03 07:37:59 +02:00
|
|
|
assert endpoint_name is not None
|
|
|
|
assert endpoint_method is not None
|
2021-05-23 13:27:41 +02:00
|
|
|
article_title = get_openapi_summary(endpoint_name, endpoint_method)
|
2023-01-31 12:11:45 +01:00
|
|
|
elif self.api_doc_view and "{generate_api_header(" in first_line:
|
2022-09-05 16:12:21 +02:00
|
|
|
api_operation = context["PAGE_METADATA_URL"].split("/api/")[1]
|
2021-05-23 13:27:41 +02:00
|
|
|
endpoint_name, endpoint_method = get_endpoint_from_operationid(api_operation)
|
|
|
|
article_title = get_openapi_summary(endpoint_name, endpoint_method)
|
2021-05-23 09:46:10 +02:00
|
|
|
else:
|
2023-01-18 19:22:59 +01:00
|
|
|
# Strip the header and then use the first line to get the article title
|
2021-05-23 09:46:10 +02:00
|
|
|
article_title = first_line.lstrip("#").strip()
|
2021-05-23 13:27:41 +02:00
|
|
|
endpoint_name = endpoint_method = None
|
2023-01-18 19:13:33 +01:00
|
|
|
if not_index_page:
|
2022-09-08 13:36:05 +02:00
|
|
|
context["PAGE_TITLE"] = f"{article_title} | {title_base}"
|
2018-12-14 23:28:00 +01:00
|
|
|
else:
|
2022-09-05 16:12:21 +02:00
|
|
|
context["PAGE_TITLE"] = title_base
|
2024-04-16 18:27:55 +02:00
|
|
|
placeholder_open_graph_description = (
|
2022-09-05 16:12:21 +02:00
|
|
|
f"REPLACEMENT_PAGE_DESCRIPTION_{int(2**24 * random.random())}"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2024-04-16 18:27:55 +02:00
|
|
|
context["PAGE_DESCRIPTION"] = placeholder_open_graph_description
|
|
|
|
|
|
|
|
def update_description(response: HttpResponse) -> None:
|
|
|
|
if placeholder_open_graph_description.encode() in response.content:
|
|
|
|
first_paragraph_text = get_content_description(
|
|
|
|
response.content, context["PAGE_METADATA_URL"]
|
|
|
|
)
|
|
|
|
response.content = response.content.replace(
|
|
|
|
placeholder_open_graph_description.encode(),
|
|
|
|
first_paragraph_text.encode(),
|
|
|
|
)
|
|
|
|
|
|
|
|
self.add_post_render_callback(update_description)
|
2018-08-18 15:42:01 +02:00
|
|
|
|
2023-04-26 02:28:23 +02:00
|
|
|
# An "article" might require the api_url_context to be rendered
|
|
|
|
api_url_context: Dict[str, Any] = {}
|
|
|
|
add_api_url_context(api_url_context, self.request)
|
|
|
|
api_url_context["run_content_validators"] = True
|
|
|
|
context["api_url_context"] = api_url_context
|
2021-05-23 13:27:41 +02:00
|
|
|
if endpoint_name and endpoint_method:
|
2023-04-26 02:28:23 +02:00
|
|
|
context["api_url_context"]["API_ENDPOINT_NAME"] = endpoint_name + ":" + endpoint_method
|
2020-07-14 02:53:18 +02:00
|
|
|
|
2023-12-18 21:57:47 +01:00
|
|
|
if sidebar_index is not None:
|
|
|
|
sidebar_html = render_markdown_path(sidebar_index)
|
|
|
|
else:
|
|
|
|
sidebar_html = ""
|
2020-07-14 02:53:18 +02:00
|
|
|
tree = html.fragment_fromstring(sidebar_html, create_parent=True)
|
|
|
|
if not context.get("page_is_policy_center", False):
|
|
|
|
home_h1 = Element("h1")
|
|
|
|
home_link = SubElement(home_h1, "a")
|
|
|
|
home_link.attrib["class"] = "no-underline"
|
|
|
|
home_link.attrib["href"] = context["doc_root"]
|
|
|
|
home_link.text = context["doc_root_title"] + " home"
|
|
|
|
tree.insert(0, home_h1)
|
|
|
|
url = context["doc_root"] + article
|
2023-08-21 23:55:47 +02:00
|
|
|
# Remove ID attributes from sidebar headings so they don't conflict with index page headings
|
|
|
|
headings = sidebar_headings(tree)
|
|
|
|
assert isinstance(headings, list)
|
|
|
|
for h in headings:
|
|
|
|
assert isinstance(h, _Element)
|
|
|
|
h.attrib.pop("id", "")
|
2020-07-14 02:53:18 +02:00
|
|
|
# Highlight current article link
|
|
|
|
links = sidebar_links(tree, url=url)
|
|
|
|
assert isinstance(links, list)
|
|
|
|
for a in links:
|
|
|
|
assert isinstance(a, _Element)
|
|
|
|
old_class = a.attrib.get("class", "")
|
|
|
|
assert isinstance(old_class, str)
|
|
|
|
a.attrib["class"] = old_class + " highlighted"
|
|
|
|
sidebar_html = "".join(html.tostring(child, encoding="unicode") for child in tree)
|
|
|
|
context["sidebar_html"] = Markup(sidebar_html)
|
|
|
|
|
2020-05-08 06:37:58 +02:00
|
|
|
add_google_analytics_context(context)
|
2016-11-09 01:45:36 +01:00
|
|
|
return context
|
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2022-07-19 17:49:23 +02:00
|
|
|
def get(
|
|
|
|
self, request: HttpRequest, *args: object, article: str = "", **kwargs: object
|
|
|
|
) -> HttpResponse:
|
2021-11-03 21:36:54 +01:00
|
|
|
# Hack: It's hard to reinitialize urls.py from tests, and so
|
|
|
|
# we want to defer the use of settings.POLICIES_DIRECTORY to
|
|
|
|
# runtime.
|
|
|
|
if self.policies_view:
|
|
|
|
self.path_template = f"{settings.POLICIES_DIRECTORY}/%s.md"
|
|
|
|
|
2021-06-03 18:17:57 +02:00
|
|
|
documentation_article = self.get_path(article)
|
|
|
|
http_status = documentation_article.article_http_status
|
2022-07-19 17:46:50 +02:00
|
|
|
result = super().get(request, article=article)
|
2024-04-16 18:27:55 +02:00
|
|
|
assert isinstance(result, TemplateResponse)
|
|
|
|
for callback in self._post_render_callbacks:
|
|
|
|
result.add_post_render_callback(callback)
|
2018-12-03 23:03:19 +01:00
|
|
|
if http_status != 200:
|
|
|
|
result.status_code = http_status
|
2016-11-09 01:45:36 +01:00
|
|
|
return result
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def add_integrations_context(context: Dict[str, Any]) -> None:
|
2017-07-05 22:07:46 +02:00
|
|
|
alphabetical_sorted_categories = OrderedDict(sorted(CATEGORIES.items()))
|
2017-02-28 07:18:45 +01:00
|
|
|
alphabetical_sorted_integration = OrderedDict(sorted(INTEGRATIONS.items()))
|
2023-09-12 23:19:57 +02:00
|
|
|
enabled_integrations_count = sum(v.is_enabled() for v in INTEGRATIONS.values())
|
2018-02-06 16:55:20 +01:00
|
|
|
# Subtract 1 so saying "Over X integrations" is correct. Then,
|
|
|
|
# round down to the nearest multiple of 10.
|
|
|
|
integrations_count_display = ((enabled_integrations_count - 1) // 10) * 10
|
2021-02-12 08:20:45 +01:00
|
|
|
context["categories_dict"] = alphabetical_sorted_categories
|
|
|
|
context["integrations_dict"] = alphabetical_sorted_integration
|
|
|
|
context["integrations_count_display"] = integrations_count_display
|
2017-02-28 07:18:45 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2019-06-15 07:19:57 +02:00
|
|
|
def add_integrations_open_graph_context(context: Dict[str, Any], request: HttpRequest) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
path_name = request.path.rstrip("/").split("/")[-1]
|
2021-02-12 08:19:30 +01:00
|
|
|
description = (
|
2021-02-12 08:20:45 +01:00
|
|
|
"Zulip comes with over a hundred native integrations out of the box, "
|
2021-06-09 21:47:43 +02:00
|
|
|
"and integrates with Zapier and IFTTT to provide hundreds more. "
|
|
|
|
"Connect the apps you use every day to Zulip."
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-06-15 07:19:57 +02:00
|
|
|
|
|
|
|
if path_name in INTEGRATIONS:
|
|
|
|
integration = INTEGRATIONS[path_name]
|
2022-09-08 13:36:05 +02:00
|
|
|
context["PAGE_TITLE"] = f"{integration.display_name} | Zulip integrations"
|
2022-09-05 16:12:21 +02:00
|
|
|
context["PAGE_DESCRIPTION"] = description
|
2019-06-15 07:19:57 +02:00
|
|
|
|
|
|
|
elif path_name in CATEGORIES:
|
|
|
|
category = CATEGORIES[path_name]
|
2022-09-08 13:36:05 +02:00
|
|
|
if path_name in META_CATEGORY:
|
|
|
|
context["PAGE_TITLE"] = f"{category} | Zulip integrations"
|
|
|
|
else:
|
|
|
|
context["PAGE_TITLE"] = f"{category} tools | Zulip integrations"
|
2022-09-05 16:12:21 +02:00
|
|
|
context["PAGE_DESCRIPTION"] = description
|
2017-02-28 07:18:45 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
elif path_name == "integrations":
|
2022-09-08 13:36:05 +02:00
|
|
|
context["PAGE_TITLE"] = "Zulip integrations"
|
2022-09-05 16:12:21 +02:00
|
|
|
context["PAGE_DESCRIPTION"] = description
|
2019-07-10 21:30:06 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2016-09-13 19:09:03 +02:00
|
|
|
class IntegrationView(ApiURLView):
|
2021-02-12 08:20:45 +01:00
|
|
|
template_name = "zerver/integrations/index.html"
|
2016-07-29 15:06:41 +02:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2017-11-27 09:28:57 +01:00
|
|
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
context: Dict[str, Any] = super().get_context_data(**kwargs)
|
2017-02-28 07:18:45 +01:00
|
|
|
add_integrations_context(context)
|
2019-06-15 07:19:57 +02:00
|
|
|
add_integrations_open_graph_context(context, self.request)
|
2020-05-08 06:37:58 +02:00
|
|
|
add_google_analytics_context(context)
|
2016-07-29 15:06:41 +02:00
|
|
|
return context
|
2016-09-13 19:18:22 +02:00
|
|
|
|
|
|
|
|
2017-07-12 02:50:27 +02:00
|
|
|
@has_request_variables
|
2021-02-12 08:19:30 +01:00
|
|
|
def integration_doc(request: HttpRequest, integration_name: str = REQ()) -> HttpResponse:
|
2022-01-13 23:35:46 +01:00
|
|
|
# FIXME: This check is jQuery-specific.
|
|
|
|
if request.headers.get("x-requested-with") != "XMLHttpRequest":
|
2018-05-11 16:35:03 +02:00
|
|
|
return HttpResponseNotFound()
|
2022-01-13 23:35:46 +01:00
|
|
|
|
2017-07-12 02:50:27 +02:00
|
|
|
try:
|
|
|
|
integration = INTEGRATIONS[integration_name]
|
|
|
|
except KeyError:
|
|
|
|
return HttpResponseNotFound()
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
context: Dict[str, Any] = {}
|
2023-04-26 02:28:23 +02:00
|
|
|
add_api_url_context(context, request)
|
2017-11-20 03:27:04 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
context["integration_name"] = integration.name
|
|
|
|
context["integration_display_name"] = integration.display_name
|
2024-05-05 22:35:19 +02:00
|
|
|
context["recommended_channel_name"] = integration.stream_name
|
2017-11-20 03:27:04 +01:00
|
|
|
if isinstance(integration, WebhookIntegration):
|
2021-02-12 08:20:45 +01:00
|
|
|
context["integration_url"] = integration.url[3:]
|
2023-08-07 12:25:33 +02:00
|
|
|
all_event_types = get_all_event_types_for_integration(integration)
|
|
|
|
if all_event_types is not None:
|
|
|
|
context["all_event_types"] = all_event_types
|
2017-11-20 03:27:04 +01:00
|
|
|
if isinstance(integration, HubotIntegration):
|
2021-02-12 08:20:45 +01:00
|
|
|
context["hubot_docs_url"] = integration.hubot_docs_url
|
2017-11-20 03:27:04 +01:00
|
|
|
|
2022-12-07 19:43:49 +01:00
|
|
|
doc_html_str = render_markdown_path(integration.doc, context, integration_doc=True)
|
2017-07-12 02:50:27 +02:00
|
|
|
|
|
|
|
return HttpResponse(doc_html_str)
|