def test_doc_api_for_redirect_to_doc(client, api_settings, root_doc, redirect_doc, cleared_cacheback_cache): """ Test the document API when we're requesting data for a document that redirects to another document. """ url = reverse('api.v1.doc', args=[redirect_doc.locale, redirect_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST, follow=True) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == root_doc.locale assert doc_data['slug'] == root_doc.slug assert doc_data['id'] == root_doc.id assert doc_data['title'] == root_doc.title assert doc_data['language'] == root_doc.language assert doc_data['hrefLang'] == 'en' assert doc_data['absoluteURL'] == root_doc.get_absolute_url() assert doc_data['wikiURL'] == absolutify(root_doc.get_absolute_url(), for_wiki_site=True) assert doc_data['translateURL'] == absolutify(reverse( 'wiki.select_locale', args=(root_doc.slug, ), locale=root_doc.locale, ), for_wiki_site=True) assert doc_data['bodyHTML'] == root_doc.get_body_html() assert doc_data['quickLinksHTML'] == root_doc.get_quick_links_html() assert doc_data['tocHTML'] == root_doc.get_toc_html() assert doc_data['translations'] == [] assert doc_data['lastModified'] == '2017-04-14T12:15:00'
def test_doc_api_for_redirect_to_doc(client, root_doc, redirect_doc): """ Test the document API when we're requesting data for a document that redirects to another document. """ url = reverse("api.v1.doc", args=[redirect_doc.locale, redirect_doc.slug]) response = client.get(url, follow=True) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data["documentData"] assert data["redirectURL"] is None doc_data = data["documentData"] assert doc_data["locale"] == root_doc.locale assert doc_data["slug"] == root_doc.slug assert doc_data["id"] == root_doc.id assert doc_data["title"] == root_doc.title assert doc_data["language"] == root_doc.language assert doc_data["hrefLang"] == "en" assert doc_data["absoluteURL"] == root_doc.get_absolute_url() assert doc_data["wikiURL"] == absolutify( root_doc.get_absolute_url(), for_wiki_site=True ) assert doc_data["translateURL"] == absolutify( reverse("wiki.select_locale", args=(root_doc.slug,), locale=root_doc.locale,), for_wiki_site=True, ) assert doc_data["bodyHTML"] == root_doc.get_body_html() assert doc_data["quickLinksHTML"] == root_doc.get_quick_links_html() assert doc_data["tocHTML"] == root_doc.get_toc_html() assert doc_data["translations"] == [] assert doc_data["lastModified"] == "2017-04-14T12:15:00"
def _send_payment_received_email(invoice, locale): user = get_user_model().objects.get(stripe_customer_id=invoice.customer) subscription_info = retrieve_and_synchronize_subscription_info(user) locale = locale or settings.WIKI_DEFAULT_LANGUAGE context = { "payment_date": datetime.fromtimestamp(invoice.created), "next_payment_date": subscription_info["next_payment_at"], "invoice_number": invoice.number, "cost": invoice.total / 100, "credit_card_brand": subscription_info["brand"], "manage_subscription_url": absolutify(reverse("payment_management")), "faq_url": absolutify(reverse("payments_index")), "contact_email": settings.CONTRIBUTION_SUPPORT_EMAIL, } with translation.override(locale): subject = render_email("users/email/payment_received/subject.ltxt", context) # Email subject *must not* contain newlines subject = "".join(subject.splitlines()) plain = render_email("users/email/payment_received/plain.ltxt", context) send_mail_retrying( subject, plain, settings.DEFAULT_FROM_EMAIL, [user.email], attachment={ "name": os.path.basename(urlparse(invoice.invoice_pdf).path), "bytes": _download_from_url(invoice.invoice_pdf), "mime": "application/pdf", }, )
def test_sitemaps(tmpdir, settings, doc_hierarchy, sitemap_storage, storage_mode): """ Test the build of the sitemaps. """ settings.SITE_URL = "https://example.com" settings.MEDIA_ROOT = str(tmpdir.mkdir("media")) settings.SITEMAP_USE_S3 = storage_mode == "s3" build_sitemaps() loc_re = re.compile(r"<loc>(.+)</loc>") lastmod_re = re.compile(r"<lastmod>(.+)</lastmod>") sitemap_file_path = sitemap_storage.joinpaths("sitemap.xml") assert sitemap_storage.exists(sitemap_file_path) with sitemap_storage.open(sitemap_file_path, "r") as file: actual_index_locs = loc_re.findall(file.read()) # Check for duplicates. assert len(actual_index_locs) == len(set(actual_index_locs)) expected_index_locs = set() for locale, _ in settings.LANGUAGES: names = ["sitemap_other.xml"] actual_locs, actual_lastmods = [], [] docs = Document.objects.filter(locale=locale) if docs.exists(): names.append("sitemap.xml") for name in names: sitemap_path = os.path.join("sitemaps", locale, name) expected_index_locs.add(absolutify(sitemap_path)) sitemap_file_path = sitemap_storage.joinpaths(sitemap_path) assert sitemap_storage.exists(sitemap_file_path) with sitemap_storage.open(sitemap_file_path, "r") as file: sitemap = file.read() actual_locs.extend(loc_re.findall(sitemap)) actual_lastmods.extend(lastmod_re.findall(sitemap)) # Check for duplicates. assert len(actual_locs) == len(set(actual_locs)) expected_locs, expected_lastmods = set(), set() expected_locs.add(absolutify(reverse("home", locale=locale))) for doc in docs: expected_locs.add(absolutify(doc.get_absolute_url())) expected_lastmods.add(doc.modified.strftime("%Y-%m-%d")) assert set(actual_locs) == expected_locs assert set(actual_lastmods) == expected_lastmods assert set(actual_index_locs) == expected_index_locs
def test_doc_api_for_redirect_to_doc(client, api_settings, root_doc, redirect_doc, cleared_cacheback_cache, ensure_contributors): """ Test the document API when we're requesting data for a document that redirects to another document. """ if ensure_contributors: # Pre-populate the cache for the call to document_api_data() # made within the view that serves the "api.v1.doc" endpoint. DocumentContributorsJob().refresh(root_doc.pk) url = reverse('api.v1.doc', args=[redirect_doc.locale, redirect_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST, follow=True) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == root_doc.locale assert doc_data['slug'] == root_doc.slug assert doc_data['id'] == root_doc.id assert doc_data['title'] == root_doc.title assert doc_data['language'] == root_doc.language assert doc_data['hrefLang'] == 'en' assert doc_data['absoluteURL'] == root_doc.get_absolute_url() assert doc_data['editURL'] == absolutify(root_doc.get_edit_url(), for_wiki_site=True) assert doc_data['translateURL'] == absolutify(reverse( 'wiki.select_locale', args=(root_doc.slug, ), locale=root_doc.locale, ), for_wiki_site=True) assert doc_data['bodyHTML'] == root_doc.get_body_html() assert doc_data['quickLinksHTML'] == root_doc.get_quick_links_html() assert doc_data['tocHTML'] == root_doc.get_toc_html() assert doc_data['translations'] == [] assert doc_data['contributors'] == (['wiki_user'] if ensure_contributors else []) assert doc_data['lastModified'] == '2017-04-14T12:15:00' assert doc_data['lastModifiedBy'] == 'wiki_user' # Clear the cache for a clean slate when calling document_api_data(). DocumentContributorsJob().delete(root_doc.pk) # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(root_doc, ensure_contributors=ensure_contributors)
def test_sitemaps(tmpdir, settings, doc_hierarchy): """ Test the build of the sitemaps. """ settings.SITE_URL = 'https://example.com' settings.MEDIA_ROOT = str(tmpdir.mkdir('media')) build_sitemaps() loc_re = re.compile(r'<loc>(.+)</loc>') lastmod_re = re.compile(r'<lastmod>(.+)</lastmod>') sitemap_file_path = os.path.join(settings.MEDIA_ROOT, 'sitemap.xml') assert os.path.exists(sitemap_file_path) with open(sitemap_file_path) as file: actual_index_locs = loc_re.findall(file.read()) # Check for duplicates. assert len(actual_index_locs) == len(set(actual_index_locs)) expected_index_locs = set() for locale, _ in settings.LANGUAGES: names = ['sitemap_other.xml'] actual_locs, actual_lastmods = [], [] docs = Document.objects.filter(locale=locale) if docs.exists(): names.append('sitemap.xml') for name in names: sitemap_path = os.path.join('sitemaps', locale, name) expected_index_locs.add(absolutify(sitemap_path)) sitemap_file_path = os.path.join(settings.MEDIA_ROOT, sitemap_path) assert os.path.exists(sitemap_file_path) with open(sitemap_file_path) as file: sitemap = file.read() actual_locs.extend(loc_re.findall(sitemap)) actual_lastmods.extend(lastmod_re.findall(sitemap)) # Check for duplicates. assert len(actual_locs) == len(set(actual_locs)) expected_locs, expected_lastmods = set(), set() expected_locs.add(absolutify(reverse('home', locale=locale))) for doc in docs: expected_locs.add(absolutify(doc.get_absolute_url())) expected_lastmods.add(doc.modified.strftime('%Y-%m-%d')) assert set(actual_locs) == expected_locs assert set(actual_lastmods) == expected_lastmods assert set(actual_index_locs) == expected_index_locs
def test_doc_api_for_redirect_to_non_doc(client, api_settings, redirect_to_home, redirect_to_macros, case): """ Test the document API when we're requesting data for a document that redirects to a non-document page (either the home page or another). """ if case == 'redirect-to-home': doc = redirect_to_home expected_redirect_url = '/en-US/' else: doc = redirect_to_macros expected_redirect_url = absolutify('/en-US/dashboards/macros', for_wiki_site=True) url = reverse('api.v1.doc', args=[doc.locale, doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] is None assert data['redirectURL'] == expected_redirect_url # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(redirect_url=expected_redirect_url)
def send_payment_received_email(stripe_customer_id, locale, timestamp, invoice_pdf): user = get_user_model().objects.get(stripe_customer_id=stripe_customer_id) locale = locale or settings.WIKI_DEFAULT_LANGUAGE context = { "payment_date": datetime.datetime.fromtimestamp(timestamp), "manage_subscription_url": absolutify(reverse("recurring_payment_management")), "faq_url": absolutify(reverse("payments_index")), "contact_email": settings.CONTRIBUTION_SUPPORT_EMAIL, "invoice_pdf": invoice_pdf, } with translation.override(locale): subject = render_email("users/email/payment_received/subject.ltxt", context) # Email subject *must not* contain newlines subject = "".join(subject.splitlines()) plain = render_email("users/email/payment_received/plain.ltxt", context) send_mail_retrying(subject, plain, settings.DEFAULT_FROM_EMAIL, [user.email])
def create_missing_stripe_webhook(): url_path = reverse("users.stripe_hooks") url = ( "https://" + settings.STRIPE_WEBHOOK_HOSTNAME + url_path if settings.STRIPE_WEBHOOK_HOSTNAME else absolutify(url_path) ) # From https://stripe.com/docs/api/webhook_endpoints/create events = ( # "Occurs whenever an invoice payment attempt succeeds." "invoice.payment_succeeded", # "Occurs whenever a customer’s subscription ends." # Also, if you go into the Stripe Dashboard, click Billing, Subscriptions, # and find a customer and click the "Cancel subscription" button, this # triggers. "customer.subscription.deleted", ) for webhook in stripe.WebhookEndpoint.list().auto_paging_iter(): if webhook.url == url and set(events) == set(webhook.enabled_events): return stripe.WebhookEndpoint.create( url=url, enabled_events=events, )
def document_api_data(doc=None, ensure_contributors=False, redirect_url=None): """ Returns the JSON data for the document for the document API. """ if doc: job = DocumentContributorsJob() # If "ensure_contributors" is True, we need the contributors since the # result will likely be cached, so we'll set "fetch_on_miss" and wait # for the result if it's not already available or stale. job.fetch_on_miss = ensure_contributors contributors = [c['username'] for c in job.get(doc.pk)] else: contributors = None return { 'locale': doc and doc.locale, 'slug': doc and doc.slug, 'id': doc and doc.id, 'title': doc and doc.title, 'summary': doc and doc.get_summary_html(), 'language': doc and doc.language, 'absoluteURL': doc and doc.get_absolute_url(), 'redirectURL': redirect_url, 'editURL': doc and absolutify(doc.get_edit_url(), for_wiki_site=True), 'bodyHTML': doc and doc.get_body_html(), 'quickLinksHTML': doc and doc.get_quick_links_html(), 'tocHTML': doc and doc.get_toc_html(), 'parents': doc and [{ 'url': d.get_absolute_url(), 'title': d.title } for d in doc.parents], 'translations': doc and [{ 'language': t.language, 'localizedLanguage': _(settings.LOCALES[t.locale].english), 'locale': t.locale, 'url': t.get_absolute_url(), 'title': t.title } for t in doc.get_other_translations(fields=('locale', 'slug', 'title'))], 'contributors': contributors, 'lastModified': (doc and doc.current_revision and doc.current_revision.created.isoformat()), 'lastModifiedBy': (doc and doc.current_revision and str(doc.current_revision.creator)) }
def test_get_content_based_redirect(root_doc, redirect_doc, redirect_to_self, redirect_to_home, redirect_to_macros, case): if case == "normal": doc = root_doc expected = None elif case == "redirect": doc = redirect_doc expected = ( get_s3_key(root_doc, prefix_with_forward_slash=True, suffix_file_extension=False), True, ) elif case == "redirect-to-self": doc = redirect_to_self expected = None elif case == "redirect-to-home": doc = redirect_to_home expected = ("/en-US/", False) else: doc = redirect_to_macros expected = (absolutify("/en-US/dashboards/macros", for_wiki_site=True), False) assert get_content_based_redirect(doc) == expected
def test_doc_api_for_redirect_to_non_doc(client, redirect_to_home, redirect_to_macros, case): """ Test the document API when we're requesting data for a document that redirects to a non-document page (either the home page or another). """ if case == 'redirect-to-home': doc = redirect_to_home expected_redirect_url = '/en-US/' else: doc = redirect_to_macros expected_redirect_url = absolutify('/en-US/dashboards/macros', for_wiki_site=True) url = reverse('api.v1.doc', args=[doc.locale, doc.slug]) response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] is None assert data['redirectURL'] == expected_redirect_url # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(redirect_url=expected_redirect_url)
def get_content_based_redirect(document): """ Returns None if the document is not a content-based redirect, otherwise a tuple pair comprising the redirect URL as well as a boolean value. The boolean value will be True if this is a redirect to another document, otherwise False. If the document is a redirect to another document or a redirect to the homepage, a relative URL will be returned, otherwise it will be a full URL to the wiki site. """ redirect_url = document.get_redirect_url() if redirect_url and (redirect_url != document.get_absolute_url()): redirect_document = document.get_redirect_document(id_only=False) if redirect_document: # This is a redirect to another document. return (get_s3_key(redirect_document, prefix_with_forward_slash=True, suffix_file_extension=False), True) # This is a redirect to non-document page. For now, if it's the home # page, return a relative path (so we stay on the read-only domain), # otherwise return the full URL for the wiki site. locale = document.locale is_home_page = (redirect_url in ('/', '/' + locale, '/{}/'.format(locale))) if is_home_page: # Let's return a relative URL to the home page for this locale. return ('/{}/'.format(locale), False) # Otherwise, let's return a full URL to the Wiki site. return (absolutify(redirect_url, for_wiki_site=True), False) return None
def test_doc_api(client, trans_doc, cleared_cacheback_cache): """On success we get document details in a JSON response.""" url = reverse('api.v1.doc', args=[trans_doc.locale, trans_doc.slug]) response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == trans_doc.locale assert doc_data['slug'] == trans_doc.slug assert doc_data['id'] == trans_doc.id assert doc_data['title'] == trans_doc.title assert doc_data['language'] == trans_doc.language assert doc_data['hrefLang'] == 'fr' assert doc_data['absoluteURL'] == trans_doc.get_absolute_url() assert doc_data['wikiURL'] == absolutify(trans_doc.get_absolute_url(), for_wiki_site=True) assert doc_data['translateURL'] is None assert doc_data['bodyHTML'] == trans_doc.get_body_html() assert doc_data['quickLinksHTML'] == trans_doc.get_quick_links_html() assert doc_data['tocHTML'] == trans_doc.get_toc_html() assert doc_data['translations'] == [{ 'locale': 'en-US', 'language': 'English (US)', 'hrefLang': 'en', 'localizedLanguage': 'Anglais am\u00e9ricain', 'title': 'Root Document', 'url': '/en-US/docs/Root' }] assert doc_data['lastModified'] == '2017-04-14T12:20:00'
def get_content_based_redirect(document): """ Returns None if the document is not a content-based redirect, otherwise a tuple pair comprising the redirect URL as well as a boolean value. The boolean value will be True if this is a redirect to another document, otherwise False. If the document is a redirect to another document or a redirect to the homepage, a relative URL will be returned, otherwise it will be a full URL to the wiki site. """ redirect_url = document.get_redirect_url() if redirect_url and (redirect_url != document.get_absolute_url()): redirect_document = document.get_redirect_document(id_only=False) if redirect_document: # This is a redirect to another document. return (get_s3_key(redirect_document, for_redirect=True), True) # This is a redirect to non-document page. For now, if it's the home # page, return a relative path (so we stay on the read-only domain), # otherwise return the full URL for the wiki site. locale = document.locale is_home_page = (redirect_url in ('/', '/' + locale, '/{}/'.format(locale))) if is_home_page: # Let's return a relative URL to the home page for this locale. return ('/{}/'.format(locale), False) # Otherwise, let's return a full URL to the Wiki site. return (absolutify(redirect_url, for_wiki_site=True), False) return None
def handle(self, *args, **options): base_url = options['baseurl'] or absolutify('') if options['nocache']: cache_control = 'no-cache' else: cache_control = 'max-age=0' force = options['force'] invalidate_cdn_cache = not options['skip_cdn_invalidation'] if options['all']: # Query all documents, excluding those whose `last_rendered_at` is # within `min_render_age` or NULL. min_render_age = ( datetime.datetime.now() - datetime.timedelta(seconds=options['min_age'])) docs = Document.objects.filter( Q(last_rendered_at__isnull=True) | Q(last_rendered_at__lt=min_render_age)) if options['locale']: docs = docs.filter(locale=options['locale']) if options['not_locale']: docs = docs.exclude(locale=options['not_locale']) docs = docs.order_by('-modified') docs = docs.values_list('id', flat=True) self.chain_render_docs( docs, cache_control, base_url, force, invalidate_cdn_cache=invalidate_cdn_cache) else: # Accept page paths from command line, but be liberal # in what we accept, eg: /en-US/docs/CSS (full path); # /en-US/CSS (no /docs); or even en-US/CSS (no leading slash) paths = options['paths'] if not paths: raise CommandError('Need at least one document path to render') for path in paths: if path.startswith('/'): path = path[1:] locale, sep, slug = path.partition('/') head, sep, tail = slug.partition('/') if head == 'docs': slug = tail doc = Document.objects.get(locale=locale, slug=slug) log.info('Rendering %s (%s)' % (doc, doc.get_absolute_url())) try: render_document( doc.pk, cache_control, base_url, force, invalidate_cdn_cache=invalidate_cdn_cache ) log.debug('DONE.') except DocumentRenderingInProgress: log.error( 'Rendering is already in progress for this document.')
def document_api_data(doc=None, ensure_contributors=False, redirect_url=None): """ Returns the JSON data for the document for the document API. """ if redirect_url: return { 'documentData': None, 'redirectURL': redirect_url, } job = DocumentContributorsJob() # If "ensure_contributors" is True, we need the contributors since the # result will likely be cached, so we'll set "fetch_on_miss" and wait # for the result if it's not already available or stale. job.fetch_on_miss = ensure_contributors contributors = [c['username'] for c in job.get(doc.pk)] return { 'documentData': { 'locale': doc.locale, 'slug': doc.slug, 'id': doc.id, 'title': doc.title, 'summary': doc.get_summary_html(), 'language': doc.language, 'absoluteURL': doc.get_absolute_url(), 'editURL': absolutify(doc.get_edit_url(), for_wiki_site=True), 'bodyHTML': doc.get_body_html(), 'quickLinksHTML': doc.get_quick_links_html(), 'tocHTML': doc.get_toc_html(), 'parents': [ { 'url': d.get_absolute_url(), 'title': d.title } for d in doc.parents ], 'translations': [ { 'language': t.language, 'localizedLanguage': _(settings.LOCALES[t.locale].english), 'locale': t.locale, 'url': t.get_absolute_url(), 'title': t.title } for t in doc.get_other_translations( fields=('locale', 'slug', 'title')) ], 'contributors': contributors, 'lastModified': (doc.current_revision and doc.current_revision.created.isoformat()), 'lastModifiedBy': (doc.current_revision and str(doc.current_revision.creator)) }, 'redirectURL': None, }
def document_api_data(document): translations = document.get_other_translations(fields=('locale', 'slug', 'title')) return { 'locale': document.locale, 'slug': document.slug, 'id': document.id, 'title': document.title, 'summary': document.get_summary_html(), 'language': document.language, 'absoluteURL': document.get_absolute_url(), 'redirectURL': document.get_redirect_url(), 'editURL': absolutify(document.get_edit_url(), for_wiki_site=True), 'bodyHTML': document.get_body_html(), 'quickLinksHTML': document.get_quick_links_html(), 'tocHTML': document.get_toc_html(), 'parents': [{ 'url': d.get_absolute_url(), 'title': d.title } for d in document.parents], 'translations': [{ 'language': t.language, 'localizedLanguage': _(settings.LOCALES[t.locale].english), 'locale': t.locale, 'url': t.get_absolute_url(), 'title': t.title } for t in translations], 'contributors': [c['username'] for c in document.contributors], 'lastModified': document.current_revision.created.isoformat(), 'lastModifiedBy': (document.current_revision.creator and str(document.current_revision.creator)) }
def test_doc_api(client, api_settings, trans_doc, cleared_cacheback_cache, ensure_contributors): """On success we get document details in a JSON response.""" if ensure_contributors: # Pre-populate the cache for the call to document_api_data() # made within the view that serves the "api.v1.doc" endpoint. DocumentContributorsJob().refresh(trans_doc.pk) url = reverse('api.v1.doc', args=[trans_doc.locale, trans_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == trans_doc.locale assert doc_data['slug'] == trans_doc.slug assert doc_data['id'] == trans_doc.id assert doc_data['title'] == trans_doc.title assert doc_data['language'] == trans_doc.language assert doc_data['hrefLang'] == 'fr' assert doc_data['absoluteURL'] == trans_doc.get_absolute_url() assert doc_data['editURL'] == absolutify(trans_doc.get_edit_url(), for_wiki_site=True) assert doc_data['translateURL'] is None assert doc_data['bodyHTML'] == trans_doc.get_body_html() assert doc_data['quickLinksHTML'] == trans_doc.get_quick_links_html() assert doc_data['tocHTML'] == trans_doc.get_toc_html() assert doc_data['translations'] == [{ 'locale': 'en-US', 'language': 'English (US)', 'hrefLang': 'en', 'localizedLanguage': u'Anglais am\u00e9ricain', 'title': 'Root Document', 'url': '/en-US/docs/Root' }] assert doc_data['contributors'] == (['wiki_user'] if ensure_contributors else []) assert doc_data['lastModified'] == '2017-04-14T12:20:00' assert doc_data['lastModifiedBy'] == 'wiki_user' # Clear the cache for a clean slate when calling document_api_data(). DocumentContributorsJob().delete(trans_doc.pk) # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(trans_doc, ensure_contributors=ensure_contributors)
def test_doc_api(client, trans_doc): """On success we get document details in a JSON response.""" url = reverse("api.v1.doc", args=[trans_doc.locale, trans_doc.slug]) response = client.get(url) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data["documentData"] assert data["redirectURL"] is None doc_data = data["documentData"] assert doc_data["locale"] == trans_doc.locale assert doc_data["slug"] == trans_doc.slug assert doc_data["id"] == trans_doc.id assert doc_data["title"] == trans_doc.title assert doc_data["language"] == trans_doc.language assert doc_data["hrefLang"] == "fr" assert doc_data["absoluteURL"] == trans_doc.get_absolute_url() assert doc_data["wikiURL"] == absolutify(trans_doc.get_absolute_url(), for_wiki_site=True) assert doc_data["editURL"] == absolutify( reverse("wiki.edit", args=(trans_doc.slug, ), locale=trans_doc.locale), for_wiki_site=True, ) assert doc_data["translateURL"] is None assert doc_data["bodyHTML"] == trans_doc.get_body_html() assert doc_data["quickLinksHTML"] == trans_doc.get_quick_links_html() assert doc_data["tocHTML"] == trans_doc.get_toc_html() assert doc_data["translations"] == [{ "locale": "en-US", "language": "English (US)", "hrefLang": "en", "localizedLanguage": "Anglais am\u00e9ricain", "title": "Root Document", "url": "/en-US/docs/Root", }] assert doc_data["lastModified"] == "2017-04-14T12:20:00"
def create_missing_stripe_webhook(): url_path = reverse("users.stripe_payment_succeeded_hook") url = ( "https://" + settings.STRIPE_WEBHOOK_HOSTNAME + url_path if settings.STRIPE_WEBHOOK_HOSTNAME else absolutify(url_path) ) event = "invoice.payment_succeeded" for webhook in stripe.WebhookEndpoint.list().auto_paging_iter(): if webhook.url == url and event in webhook.enabled_events: return stripe.WebhookEndpoint.create( url=url, enabled_events=[event], )
def test_doc_api(client, api_settings, trans_doc, cleared_cacheback_cache, ensure_contributors): """On success we get document details in a JSON response.""" if ensure_contributors: # Pre-populate the cache for the call to document_api_data() # made within the view that serves the "api.v1.doc" endpoint. DocumentContributorsJob().refresh(trans_doc.pk) url = reverse('api.v1.doc', args=[trans_doc.locale, trans_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == trans_doc.locale assert doc_data['slug'] == trans_doc.slug assert doc_data['id'] == trans_doc.id assert doc_data['title'] == trans_doc.title assert doc_data['language'] == trans_doc.language assert doc_data['absoluteURL'] == trans_doc.get_absolute_url() assert doc_data['editURL'] == absolutify(trans_doc.get_edit_url(), for_wiki_site=True) assert doc_data['bodyHTML'] == trans_doc.get_body_html() assert doc_data['quickLinksHTML'] == trans_doc.get_quick_links_html() assert doc_data['tocHTML'] == trans_doc.get_toc_html() assert doc_data['translations'] == [{ 'locale': 'en-US', 'language': 'English (US)', 'localizedLanguage': u'Anglais am\u00e9ricain', 'title': 'Root Document', 'url': '/en-US/docs/Root' }] assert doc_data['contributors'] == ( ['wiki_user'] if ensure_contributors else []) assert doc_data['lastModified'] == '2017-04-14T12:20:00' assert doc_data['lastModifiedBy'] == 'wiki_user' # Clear the cache for a clean slate when calling document_api_data(). DocumentContributorsJob().delete(trans_doc.pk) # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data( trans_doc, ensure_contributors=ensure_contributors)
def test_publish_redirect_to_other(get_s3_bucket_mock, redirect_to_macros): """ Test the publish task for a document that redirects to a URL outside the S3 bucket, in this case someting other than the home page. """ log_mock = mock.Mock() get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() publish([redirect_to_macros.pk], log=log_mock) s3_bucket_mock.put_object.assert_called_once_with( ACL='public-read', Key=get_s3_key(redirect_to_macros), Body=json.dumps( document_api_data(redirect_url=absolutify( '/en-US/dashboards/macros', for_wiki_site=True))), ContentType='application/json', ContentLanguage=redirect_to_macros.locale) log_mock.info.assert_called_once_with('Published S3 Object #1')
def test_doc_api_for_redirect_to_doc(client, api_settings, root_doc, redirect_doc, cleared_cacheback_cache, ensure_contributors): """ Test the document API when we're requesting data for a document that redirects to another document. """ if ensure_contributors: # Pre-populate the cache for the call to document_api_data() # made within the view that serves the "api.v1.doc" endpoint. DocumentContributorsJob().refresh(root_doc.pk) url = reverse('api.v1.doc', args=[redirect_doc.locale, redirect_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST, follow=True) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['documentData'] assert data['redirectURL'] is None doc_data = data['documentData'] assert doc_data['locale'] == root_doc.locale assert doc_data['slug'] == root_doc.slug assert doc_data['id'] == root_doc.id assert doc_data['title'] == root_doc.title assert doc_data['language'] == root_doc.language assert doc_data['absoluteURL'] == root_doc.get_absolute_url() assert doc_data['editURL'] == absolutify(root_doc.get_edit_url(), for_wiki_site=True) assert doc_data['bodyHTML'] == root_doc.get_body_html() assert doc_data['quickLinksHTML'] == root_doc.get_quick_links_html() assert doc_data['tocHTML'] == root_doc.get_toc_html() assert doc_data['translations'] == [] assert doc_data['contributors'] == ( ['wiki_user'] if ensure_contributors else []) assert doc_data['lastModified'] == '2017-04-14T12:15:00' assert doc_data['lastModifiedBy'] == 'wiki_user' # Clear the cache for a clean slate when calling document_api_data(). DocumentContributorsJob().delete(root_doc.pk) # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data( root_doc, ensure_contributors=ensure_contributors)
def test_publish_redirect_to_other(get_s3_bucket_mock, redirect_to_macros): """ Test the publish task for a document that redirects to a URL outside the S3 bucket, in this case someting other than the home page. """ log_mock = mock.Mock() get_s3_bucket_mock.return_value = s3_bucket_mock = get_mocked_s3_bucket() publish([redirect_to_macros.pk], log=log_mock) s3_bucket_mock.put_object.assert_called_once_with( ACL='public-read', Key=get_s3_key(redirect_to_macros), Body=json.dumps(document_api_data( redirect_url=absolutify('/en-US/dashboards/macros', for_wiki_site=True))), ContentType='application/json', ContentLanguage=redirect_to_macros.locale ) log_mock.info.assert_called_once_with('Published S3 Object #1')
def handle(self, *args, **options): self.options = options self.base_url = options['baseurl'] or absolutify('') if self.options['nocache']: self.cache_control = 'no-cache' else: self.cache_control = 'max-age=0' if options['all']: # Query all documents, excluding those whose `last_rendered_at` is # within `min_render_age` or NULL. min_render_age = ( datetime.datetime.now() - datetime.timedelta(seconds=self.options['min_age'])) docs = Document.objects.filter( Q(last_rendered_at__isnull=True) | Q(last_rendered_at__lt=min_render_age)) docs = docs.order_by('-modified') docs = docs.values_list('id', flat=True) self.chain_render_docs(docs) else: if not len(args) == 1: raise CommandError('Need at least one document path to render') for path in args: # Accept a single page path from command line, but be liberal # in what we accept, eg: /en-US/docs/CSS (full path); # /en-US/CSS (no /docs); or even en-US/CSS (no leading slash) if path.startswith('/'): path = path[1:] locale, sep, slug = path.partition('/') head, sep, tail = slug.partition('/') if head == 'docs': slug = tail doc = Document.objects.get(locale=locale, slug=slug) log.info(u'Rendering {0!s} ({1!s})'.format(doc, doc.get_absolute_url())) try: render_document(doc.pk, self.cache_control, self.base_url, self.options['force']) log.debug(u'DONE.') except DocumentRenderingInProgress: log.error( u'Rendering is already in progress for this document.')
def handle(self, *args, **options): self.options = options self.base_url = options['baseurl'] or absolutify('') if self.options['nocache']: self.cache_control = 'no-cache' else: self.cache_control = 'max-age=0' if options['all']: # Query all documents, excluding those whose `last_rendered_at` is # within `min_render_age` or NULL. min_render_age = ( datetime.datetime.now() - datetime.timedelta(seconds=self.options['min_age'])) docs = Document.objects.filter( Q(last_rendered_at__isnull=True) | Q(last_rendered_at__lt=min_render_age)) docs = docs.order_by('-modified') docs = docs.values_list('id', flat=True) self.chain_render_docs(docs) else: if not len(args) == 1: raise CommandError('Need at least one document path to render') for path in args: # Accept a single page path from command line, but be liberal # in what we accept, eg: /en-US/docs/CSS (full path); # /en-US/CSS (no /docs); or even en-US/CSS (no leading slash) if path.startswith('/'): path = path[1:] locale, sep, slug = path.partition('/') head, sep, tail = slug.partition('/') if head == 'docs': slug = tail doc = Document.objects.get(locale=locale, slug=slug) log.info(u'Rendering %s (%s)' % (doc, doc.get_absolute_url())) try: render_document(doc.pk, self.cache_control, self.base_url, self.options['force']) log.debug(u'DONE.') except DocumentRenderingInProgress: log.error( u'Rendering is already in progress for this document.')
def create_sendinblue_unsubscribe_webhook(): url_path = reverse("api.v1.sendinblue_hooks") url = ("https://" + settings.CUSTOM_WEBHOOK_HOSTNAME + url_path if settings.CUSTOM_WEBHOOK_HOSTNAME else absolutify(url_path)) # From https://developers.sendinblue.com/reference#createwebhook # "Occurs whenever a user unsubscribes through an email's subscription management link." events = ("unsubscribed", ) response = sendinblue.request("GET", "webhooks", params={"type": EMAIL_TYPE}) no_webhooks_found = response.status_code == 404 if not response.ok and not no_webhooks_found: return [ Error( f"Error getting sendinblue webhooks: {response.status_code}", id=SENDINBLUE_API_ERROR, ) ] if not no_webhooks_found: for webhook in response.json()["webhooks"]: if webhook["url"] == url and set(events) == set(webhook["events"]): return [] response = sendinblue.request("POST", "webhooks", json={ "url": url, "events": events, "type": EMAIL_TYPE }) if not response.ok: return [ Error( f"Error creating sendinblue webhook: {response.status_code}", id=SENDINBLUE_API_ERROR, ) ] return []
def test_get_content_based_redirect(root_doc, redirect_doc, redirect_to_self, redirect_to_home, redirect_to_macros, case): if case == 'normal': doc = root_doc expected = None elif case == 'redirect': doc = redirect_doc expected = (get_s3_key(root_doc, for_redirect=True), True) elif case == 'redirect-to-self': doc = redirect_to_self expected = None elif case == 'redirect-to-home': doc = redirect_to_home expected = ('/en-US/', False) else: doc = redirect_to_macros expected = ( absolutify('/en-US/dashboards/macros', for_wiki_site=True), False) assert get_content_based_redirect(doc) == expected
def test_get_content_based_redirect(root_doc, redirect_doc, redirect_to_self, redirect_to_home, redirect_to_macros, case): if case == 'normal': doc = root_doc expected = None elif case == 'redirect': doc = redirect_doc expected = (get_s3_key(root_doc, prefix_with_forward_slash=True), True) elif case == 'redirect-to-self': doc = redirect_to_self expected = None elif case == 'redirect-to-home': doc = redirect_to_home expected = ('/en-US/', False) else: doc = redirect_to_macros expected = ( absolutify('/en-US/dashboards/macros', for_wiki_site=True), False) assert get_content_based_redirect(doc) == expected
def test_doc_api(client, api_settings, trans_doc, cleared_cacheback_cache): """On success we get document details in a JSON response.""" # The fetch_on_miss mock and the cleared_cacheback_cache fixture # are here to ensure that we don't get an old cached value for # the contributors property, and also that we don't use [] # while a celery job is running. url = reverse('api.v1.doc', args=[trans_doc.locale, trans_doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['locale'] == trans_doc.locale assert data['slug'] == trans_doc.slug assert data['id'] == trans_doc.id assert data['title'] == trans_doc.title assert data['language'] == trans_doc.language assert data['absoluteURL'] == trans_doc.get_absolute_url() assert data['redirectURL'] == trans_doc.get_redirect_url() assert data['editURL'] == absolutify(trans_doc.get_edit_url(), for_wiki_site=True) assert data['bodyHTML'] == trans_doc.get_body_html() assert data['quickLinksHTML'] == trans_doc.get_quick_links_html() assert data['tocHTML'] == trans_doc.get_toc_html() assert data['translations'] == [{ 'locale': 'en-US', 'language': 'English (US)', 'localizedLanguage': u'Anglais am\u00e9ricain', 'title': 'Root Document', 'url': '/en-US/docs/Root' }] assert data['contributors'] == ['wiki_user'] assert data['lastModified'] == '2017-04-14T12:20:00' assert data['lastModifiedBy'] == 'wiki_user' # Also ensure that we get exactly the same data by calling # the document_api_data() function directly data2 = document_api_data(trans_doc) assert data == data2
def test_doc_api_for_redirect_to_non_doc(client, api_settings, redirect_to_home, redirect_to_macros, case): """ Test the document API when we're requesting data for a document that redirects to a non-document page (either the home page or another). """ if case == 'redirect-to-home': doc = redirect_to_home expected_redirect_url = '/en-US/' else: doc = redirect_to_macros expected_redirect_url = absolutify('/en-US/dashboards/macros', for_wiki_site=True) url = reverse('api.v1.doc', args=[doc.locale, doc.slug]) response = client.get(url, HTTP_HOST=api_settings.BETA_HOST) assert response.status_code == 200 assert_no_cache_header(response) data = response.json() assert data['locale'] is None assert data['slug'] is None assert data['id'] is None assert data['title'] is None assert data['language'] is None assert data['absoluteURL'] is None assert data['redirectURL'] == expected_redirect_url assert data['editURL'] is None assert data['bodyHTML'] is None assert data['quickLinksHTML'] is None assert data['tocHTML'] is None assert data['translations'] is None assert data['contributors'] is None assert data['lastModified'] is None assert data['lastModifiedBy'] is None # Also ensure that we get exactly the same data by calling # the document_api_data() function directly assert data == document_api_data(redirect_url=expected_redirect_url)
def payments_thank_you_email(username, user_email, recurring=False): """Create a notification email for new contributor.""" message_context = { 'user_email': user_email, 'username': username, 'support_mail_link': 'mailto:' + settings.CONTRIBUTION_SUPPORT_EMAIL + '?Subject=Recurring%20payment%20support', 'support_mail': settings.CONTRIBUTION_SUPPORT_EMAIL, 'recurring_payment': recurring, 'recurring_payment_management': absolutify(reverse('recurring_payment_management')), } # TODO: Remove when we ship translations, get legal approval locale = settings.WIKI_DEFAULT_LANGUAGE log.debug('Using the locale %s to send the contribution thank you email', locale) with translation.override(locale): subject = render_email( 'payments/email/thank_you/subject.ltxt', message_context ) content_plain = render_email( 'payments/email/thank_you/plain.ltxt', message_context ) content_html = render_email( 'payments/email/thank_you/email.html', message_context ) email = EmailMultiAlternatives( subject, content_plain, settings.DEFAULT_FROM_EMAIL, [user_email] ) email.attach_alternative(content_html, 'text/html') email.send()
def document_api_data(doc=None, redirect_url=None): """ Returns the JSON data for the document for the document API. """ if redirect_url: return { 'documentData': None, 'redirectURL': redirect_url, } # The original english slug for this document, for google analytics if doc.locale == 'en-US': en_slug = doc.slug elif doc.parent_id and doc.parent.locale == 'en-US': en_slug = doc.parent.slug else: en_slug = '' other_translations = doc.get_other_translations(fields=('locale', 'slug', 'title', 'parent')) available_locales = ({doc.locale} | set(t.locale for t in other_translations)) doc_absolute_url = doc.get_absolute_url() revision = doc.current_or_latest_revision() translation_status = None if doc.parent_id and revision and revision.localization_in_progress: translation_status = 'outdated' if revision.translation_age >= 10 else 'in-progress' return { 'documentData': { 'locale': doc.locale, 'slug': doc.slug, 'enSlug': en_slug, 'id': doc.id, 'title': doc.title, 'summary': doc.get_summary_html(), 'language': doc.language, 'hrefLang': doc.get_hreflang(available_locales), 'absoluteURL': doc_absolute_url, 'wikiURL': absolutify(doc_absolute_url, for_wiki_site=True), 'translateURL': (absolutify(reverse( 'wiki.select_locale', args=(doc.slug, ), locale=doc.locale, ), for_wiki_site=True) if doc.is_localizable else None), 'translationStatus': translation_status, 'bodyHTML': doc.get_body_html(), 'quickLinksHTML': doc.get_quick_links_html(), 'tocHTML': doc.get_toc_html(), 'raw': doc.html, 'parents': [{ 'url': d.get_absolute_url(), 'title': d.title } for d in doc.parents], 'translations': [{ 'language': t.language, 'hrefLang': t.get_hreflang(available_locales), 'localizedLanguage': _(settings.LOCALES[t.locale].english), 'locale': t.locale, 'url': t.get_absolute_url(), 'title': t.title } for t in other_translations], 'lastModified': (doc.current_revision and doc.current_revision.created.isoformat()), }, 'redirectURL': None, }
def document_api_data(doc=None, ensure_contributors=False, redirect_url=None): """ Returns the JSON data for the document for the document API. """ if redirect_url: return { 'documentData': None, 'redirectURL': redirect_url, } job = DocumentContributorsJob() # If "ensure_contributors" is True, we need the contributors since the # result will likely be cached, so we'll set "fetch_on_miss" and wait # for the result if it's not already available or stale. job.fetch_on_miss = ensure_contributors contributors = [c['username'] for c in job.get(doc.pk)] # The original english slug for this document, for google analytics if doc.locale == 'en-US': en_slug = doc.slug elif doc.parent_id and doc.parent.locale == 'en-US': en_slug = doc.parent.slug else: en_slug = '' other_translations = doc.get_other_translations(fields=('locale', 'slug', 'title')) available_locales = (set([doc.locale]) | set(t.locale for t in other_translations)) return { 'documentData': { 'locale': doc.locale, 'slug': doc.slug, 'enSlug': en_slug, 'id': doc.id, 'title': doc.title, 'summary': doc.get_summary_html(), 'language': doc.language, 'hrefLang': doc.get_hreflang(available_locales), 'absoluteURL': doc.get_absolute_url(), 'editURL': absolutify(doc.get_edit_url(), for_wiki_site=True), 'translateURL': (absolutify(reverse( 'wiki.select_locale', args=(doc.slug, ), locale=doc.locale, ), for_wiki_site=True) if doc.is_localizable else None), 'bodyHTML': doc.get_body_html(), 'quickLinksHTML': doc.get_quick_links_html(), 'tocHTML': doc.get_toc_html(), 'parents': [{ 'url': d.get_absolute_url(), 'title': d.title } for d in doc.parents], 'translations': [{ 'language': t.language, 'hrefLang': t.get_hreflang(available_locales), 'localizedLanguage': _(settings.LOCALES[t.locale].english), 'locale': t.locale, 'url': t.get_absolute_url(), 'title': t.title } for t in other_translations], 'contributors': contributors, 'lastModified': (doc.current_revision and doc.current_revision.created.isoformat()), 'lastModifiedBy': (doc.current_revision and str(doc.current_revision.creator)) }, 'redirectURL': None, }
def handle(self, *args, **options): base_url = options["baseurl"] or absolutify("") if options["nocache"]: cache_control = "no-cache" else: cache_control = "max-age=0" force = options["force"] invalidate_cdn_cache = not options["skip_cdn_invalidation"] if options["all"]: # Query all documents, excluding those whose `last_rendered_at` is # within `min_render_age` or NULL. min_render_age = datetime.datetime.now() - datetime.timedelta( seconds=options["min_age"]) docs = Document.objects.filter( Q(last_rendered_at__isnull=True) | Q(last_rendered_at__lt=min_render_age)) if options["locale"]: docs = docs.filter(locale=options["locale"]) if options["not_locale"]: docs = docs.exclude(locale=options["not_locale"]) if options["slugsearch"]: if options["slugsearch"].endswith("*"): docs = docs.exclude( slug__startswith=options["slugsearch"].rstrip("*")) elif "*" in options["slugsearch"]: raise NotImplementedError("* can only be on the end") else: docs = docs.exclude(slug__contains=options["slugsearch"]) docs = docs.order_by("-modified") docs = docs.values_list("id", flat=True) self.chain_render_docs( docs, cache_control, base_url, force, invalidate_cdn_cache=invalidate_cdn_cache, ) else: # Accept page paths from command line, but be liberal # in what we accept, eg: /en-US/docs/CSS (full path); # /en-US/CSS (no /docs); or even en-US/CSS (no leading slash) paths = options["paths"] if not paths: raise CommandError("Need at least one document path to render") for path in paths: if path.startswith("/"): path = path[1:] locale, sep, slug = path.partition("/") head, sep, tail = slug.partition("/") if head == "docs": slug = tail doc = Document.objects.get(locale=locale, slug=slug) log.info(f"Rendering {doc} ({doc.get_absolute_url()})") try: render_document( doc.pk, cache_control, base_url, force, invalidate_cdn_cache=invalidate_cdn_cache, ) log.debug("DONE.") except DocumentRenderingInProgress: log.error( "Rendering is already in progress for this document.")
def document_api_data(doc=None, redirect_url=None): """ Returns the JSON data for the document for the document API. """ if redirect_url: return { "documentData": None, "redirectURL": redirect_url, } # The original english slug for this document, for google analytics if doc.locale == "en-US": en_slug = doc.slug elif doc.parent_id and doc.parent.locale == "en-US": en_slug = doc.parent.slug else: en_slug = "" other_translations = doc.get_other_translations(fields=("locale", "slug", "title", "parent")) available_locales = {doc.locale} | set(t.locale for t in other_translations) doc_absolute_url = doc.get_absolute_url() revision = doc.current_or_latest_revision() translation_status = None if doc.parent_id and revision and revision.localization_in_progress: translation_status = ("outdated" if revision.translation_age >= 10 else "in-progress") return { "documentData": { "locale": doc.locale, "slug": doc.slug, "enSlug": en_slug, "id": doc.id, "title": doc.title, "summary": doc.get_summary_html(), "language": doc.language, "hrefLang": doc.get_hreflang(available_locales), "absoluteURL": doc_absolute_url, "wikiURL": absolutify(doc_absolute_url, for_wiki_site=True), "editURL": absolutify( reverse("wiki.edit", args=(doc.slug, ), locale=doc.locale), for_wiki_site=True, ), "translateURL": (absolutify( reverse("wiki.select_locale", args=(doc.slug, ), locale=doc.locale), for_wiki_site=True, ) if doc.is_localizable else None), "translationStatus": translation_status, "bodyHTML": doc.get_body_html(), "quickLinksHTML": doc.get_quick_links_html(), "tocHTML": doc.get_toc_html(), "raw": doc.html, "parents": [{ "url": d.get_absolute_url(), "title": d.title } for d in doc.parents], "translations": [{ "language": t.language, "hrefLang": t.get_hreflang(available_locales), "localizedLanguage": gettext(settings.LOCALES[t.locale].english), "locale": t.locale, "url": t.get_absolute_url(), "title": t.title, } for t in other_translations], "lastModified": (doc.current_revision and doc.current_revision.created.isoformat()), }, "redirectURL": None, }