Browse Source

Commit missing files

rafalp 6 years ago
parent
commit
766d04bdcf

+ 106 - 0
misago/admin/src/versionCheck.js

@@ -0,0 +1,106 @@
+import ApolloClient, { gql } from "apollo-boost"
+import React from "react"
+import ReactDOM from "react-dom"
+import { ApolloProvider, Query } from "react-apollo"
+
+const initVersionCheck = ({ elementId, errorMessage, loadingMessage, uri }) => {
+  const element = document.getElementById(elementId)
+  if (!element) console.error("Element with id " + element + "doesn't exist!")
+
+  const client = new ApolloClient({
+    credentials: "same-origin",
+    uri: uri
+  })
+
+  ReactDOM.render(
+    <ApolloProvider client={client}>
+      <VersionCheck
+        errorMessage={errorMessage}
+        loadingMessage={loadingMessage}
+      />
+    </ApolloProvider>,
+    element
+  )
+}
+
+const getVersion = gql`
+  query getVersion {
+    version {
+      status
+      message
+      description
+    }
+  }
+`
+
+const VersionCheck = ({ errorMessage, loadingMessage }) => {
+  return (
+    <Query query={getVersion}>
+      {({ loading, error, data }) => {
+        if (loading) return <Spinner {...loadingMessage} />
+        if (error) return <Error {...errorMessage} />
+
+        return <CheckMessage {...data.version} />
+      }}
+    </Query>
+  )
+}
+
+const Spinner = ({ description, message }) => (
+  <div className="media media-admin-check">
+    <div className="media-check-icon">
+      <div className="spinner-border" role="status">
+        <span className="sr-only">Loading...</span>
+      </div>
+    </div>
+    <div className="media-body">
+      <h5>{message}</h5>
+      {description}
+    </div>
+  </div>
+)
+
+const Error = ({ description, message }) => (
+  <div className="media media-admin-check">
+    <div className="media-check-icon media-check-icon-danger">
+      <span className="fas fa-times" />
+    </div>
+    <div className="media-body">
+      <h5>{message}</h5>
+      {description}
+    </div>
+  </div>
+)
+
+const CheckMessage = ({ description, message, status }) => (
+  <div className="media media-admin-check">
+    <CheckIcon status={status} />
+    <div className="media-body">
+      <h5>{message}</h5>
+      {description}
+    </div>
+  </div>
+)
+
+const CheckIcon = ({ status }) => {
+  let className = "media-check-icon media-check-icon-"
+  if (status === "SUCCESS") className += "success"
+  if (status === "WARNING") className += "warning"
+  if (status === "ERROR") className += "danger"
+
+  return (
+    <div className={className}>
+      <CheckIconImage status={status} />
+    </div>
+  )
+}
+
+const CheckIconImage = ({ status }) => {
+  if (status === "SUCCESS") return <span className="fas fa-check" />
+  if (status === "WARNING") return <span className="fas fa-question" />
+  if (status === "ERROR") return <span className="fas fa-times" />
+
+  return null
+}
+
+export default initVersionCheck

+ 12 - 0
misago/graphql/admin/status.py

@@ -0,0 +1,12 @@
+from enum import IntEnum
+
+from ariadne import EnumType
+
+
+class Status(IntEnum):
+    ERROR = 0
+    WARNING = 1
+    SUCCESS = 2
+
+
+status = EnumType("Status", Status)

+ 81 - 0
misago/graphql/admin/tests/test_version_check.py

@@ -0,0 +1,81 @@
+from unittest.mock import ANY, Mock
+
+import pytest
+from ariadne import gql
+from requests.exceptions import RequestException
+
+from .... import __version__
+from ..versioncheck import CACHE_KEY, CACHE_LENGTH, resolve_version
+
+test_query = gql("{ version { status message description } }")
+
+
+def mock_requests_get(mocker, mock):
+    return mocker.patch("requests.get", return_value=Mock(json=mock))
+
+
+def test_version_check_query_returns_error_if_misago_version_is_unreleased(
+    admin_graphql_client, mocker
+):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", False)
+    mock_requests_get(mocker, Mock(return_value={"info": {"version": "outdated"}}))
+    result = admin_graphql_client.query(test_query)
+    assert result["version"]["status"] == "ERROR"
+
+
+def test_version_check_query_returns_success_if_site_is_updated(
+    admin_graphql_client, mocker
+):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    mock_requests_get(mocker, Mock(return_value={"info": {"version": __version__}}))
+    result = admin_graphql_client.query(test_query)
+    assert result["version"]["status"] == "SUCCESS"
+
+
+def test_version_check_query_returns_error_if_site_is_outdated(
+    admin_graphql_client, mocker
+):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    mock_requests_get(mocker, Mock(return_value={"info": {"version": "outdated"}}))
+    result = admin_graphql_client.query(test_query)
+    assert result["version"]["status"] == "ERROR"
+
+
+def test_version_check_query_returns_warning_if_version_check_failed(
+    admin_graphql_client, mocker
+):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    mock_requests_get(mocker, Mock(side_effect=RequestException()))
+    result = admin_graphql_client.query(test_query)
+    assert result["version"]["status"] == "WARNING"
+
+
+def test_version_check_result_is_cached(mocker):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    set_cache = mocker.patch("django.core.cache.cache.set")
+    mock_requests_get(mocker, Mock(return_value={"info": {"version": "outdated"}}))
+    resolve_version()
+    set_cache.assert_called_with(CACHE_KEY, ANY, CACHE_LENGTH)
+
+
+def test_failed_version_check_result_is_not_cached(admin_graphql_client, mocker):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    set_cache = mocker.patch("django.core.cache.cache.set")
+    mock_requests_get(mocker, Mock(side_effect=RequestException()))
+    resolve_version()
+    set_cache.assert_not_called()
+
+
+def test_remote_api_is_not_called_if_version_check_cache_is_available(mocker):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    mocker.patch("django.core.cache.cache.get", return_value={"status": "TEST"})
+    api_mock = mock_requests_get(mocker, Mock())
+    resolve_version()
+    api_mock.assert_not_called()
+
+
+def test_version_check_cache_is_returned_when_set(mocker):
+    mocker.patch("misago.graphql.admin.versioncheck.__released__", True)
+    mocker.patch("django.core.cache.cache.get", return_value={"status": "TEST"})
+    api_mock = mock_requests_get(mocker, Mock())
+    assert resolve_version() == {"status": "TEST"}

+ 80 - 0
misago/graphql/admin/versioncheck.py

@@ -0,0 +1,80 @@
+import requests
+from ariadne import QueryType
+from django.core.cache import cache
+from django.utils.translation import gettext as _
+from requests.exceptions import RequestException
+
+from ... import __released__, __version__
+from .status import Status
+
+CACHE_KEY = "misago_admin_version_check"
+CACHE_LENGTH = 3600 * 8  # 4 hours
+
+version_check = QueryType()
+
+
+@version_check.field("version")
+def resolve_version(*_):
+    if not __released__:
+        return get_unreleased_error()
+
+    data = cache.get(CACHE_KEY)
+    if not data:
+        data = check_version_with_api()
+        if data["status"] != Status.WARNING:
+            cache.set(CACHE_KEY, data, CACHE_LENGTH)
+    return data
+
+
+def get_unreleased_error():
+    return {
+        "status": Status.ERROR,
+        "message": _("The site is running using unreleased version of Misago."),
+        "description": _(
+            "Unreleased versions of Misago can lack security features and there is "
+            "no supported way to upgrade them to release versions later."
+        ),
+    }
+
+
+def check_version_with_api():
+    try:
+        latest_version = get_latest_version()
+        return compare_versions(__version__, latest_version)
+    except (RequestException, KeyError, ValueError):
+        return {
+            "status": Status.WARNING,
+            "message": _("Failed to connect to pypi.org API. Try again later."),
+            "description": _(
+                "Version check feature relies on the API operated by the Python "
+                "Package Index (pypi.org) API to retrieve latest Misago release "
+                "version."
+            ),
+        }
+
+
+def get_latest_version():
+    api_url = "https://pypi.org/pypi/Misago/json"
+    r = requests.get(api_url)
+    r.raise_for_status()
+    return r.json()["info"]["version"]
+
+
+def compare_versions(current, latest):
+    if latest == current:
+        return {
+            "status": Status.SUCCESS,
+            "message": _("The site is running updated version of Misago."),
+            "description": _("Misago %(version)s is latest release.")
+            % {"version": current},
+        }
+
+    return {
+        "status": Status.ERROR,
+        "message": _("The site is running outdated version of Misago."),
+        "description": _(
+            "The site is running Misago version %(version)s while version %(latest)s "
+            "is available."
+        )
+        % {"version": current, "latest": latest},
+    }

+ 30 - 0
misago/users/migrations/0018_auto_20190511_2051.py

@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-05-11 20:51
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("misago_users", "0017_move_bans_to_cache_version")]
+
+    operations = [
+        migrations.AlterField(
+            model_name="datadownload",
+            name="requested_on",
+            field=models.DateTimeField(
+                db_index=True, default=django.utils.timezone.now
+            ),
+        ),
+        migrations.AlterField(
+            model_name="user",
+            name="joined_on",
+            field=models.DateTimeField(
+                db_index=True,
+                default=django.utils.timezone.now,
+                verbose_name="joined on",
+            ),
+        ),
+    ]