diff --git a/cypress/integration/root.js b/cypress/integration/root.js index 298b79d..0559b17 100644 --- a/cypress/integration/root.js +++ b/cypress/integration/root.js @@ -22,8 +22,8 @@ describe("Root", () => { it("example", () => { cy.createAdminUnit("test@test.de", "Goslar").then(function (adminUnitId) { cy.createEvent(adminUnitId).then(function (eventId) { - cy.visit("/example"); - cy.screenshot("example"); + cy.visit("/organizations"); + cy.screenshot("organizations"); }); }); }); diff --git a/project/__init__.py b/project/__init__.py index 44c6239..763986c 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -183,6 +183,7 @@ from project.views import ( oauth, oauth2_client, oauth2_token, + organization, organizer, planing, reference, diff --git a/project/access.py b/project/access.py index 50d4b95..ff6b14e 100644 --- a/project/access.py +++ b/project/access.py @@ -199,6 +199,17 @@ def can_create_admin_unit(): return any(admin_unit.can_create_other for admin_unit in admin_units) +def can_verify_admin_unit(): + if not current_user.is_authenticated: # pragma: no cover + return False + + if has_current_user_role("admin"): + return True + + admin_units = get_admin_units_for_manage() + return any(admin_unit.can_verify_other for admin_unit in admin_units) + + def can_read_event(event: Event) -> bool: if event.public_status == PublicStatus.published and event.admin_unit.is_verified: return True diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 6230198..ae7ba5f 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -5,7 +5,9 @@ from sqlalchemy import and_ from project import db from project.access import ( access_or_401, + can_verify_admin_unit, get_admin_unit_for_manage_or_404, + login_api_user, login_api_user_or_401, ) from project.api import add_api_resource @@ -166,9 +168,14 @@ class OrganizationListResource(BaseResource): @doc(summary="List organizations", tags=["Organizations"]) @use_kwargs(OrganizationListRequestSchema, location=("query")) @marshal_with(OrganizationListResponseSchema) + @require_oauth(optional=True) def get(self, **kwargs): keyword = kwargs["keyword"] if "keyword" in kwargs else None - pagination = get_admin_unit_query(keyword).paginate() + + login_api_user() + include_unverified = can_verify_admin_unit() + + pagination = get_admin_unit_query(keyword, include_unverified).paginate() return pagination diff --git a/project/api/organization/schemas.py b/project/api/organization/schemas.py index 4d3a7f2..ef67c46 100644 --- a/project/api/organization/schemas.py +++ b/project/api/organization/schemas.py @@ -37,6 +37,7 @@ class OrganizationBaseSchema(OrganizationIdSchema): email = marshmallow.auto_field() phone = marshmallow.auto_field() fax = marshmallow.auto_field() + is_verified = fields.Boolean() class OrganizationSchema(OrganizationBaseSchema): diff --git a/project/services/admin_unit.py b/project/services/admin_unit.py index 645d4a5..ddab9d0 100644 --- a/project/services/admin_unit.py +++ b/project/services/admin_unit.py @@ -14,7 +14,6 @@ from project.models import ( ) from project.services.image import upsert_image_with_data from project.services.location import assign_location_values -from project.utils import strings_are_equal_ignoring_case def insert_admin_unit_for_user(admin_unit, user, invitation=None): @@ -59,18 +58,12 @@ def insert_admin_unit_for_user(admin_unit, user, invitation=None): relation = upsert_admin_unit_relation(invitation.admin_unit_id, admin_unit.id) relation.invited = True - name_equals_suggested_name = strings_are_equal_ignoring_case( - admin_unit.name, invitation.admin_unit_name - ) relation.auto_verify_event_reference_requests = ( inviting_admin_unit.incoming_reference_requests_allowed and invitation.relation_auto_verify_event_reference_requests - and name_equals_suggested_name ) relation.verify = ( - inviting_admin_unit.can_verify_other - and invitation.relation_verify - and name_equals_suggested_name + inviting_admin_unit.can_verify_other and invitation.relation_verify ) db.session.commit() @@ -173,9 +166,12 @@ def get_admin_unit_member(id): return AdminUnitMember.query.filter_by(id=id).first() -def get_admin_unit_query(keyword=None): +def get_admin_unit_query(keyword=None, include_unverified=False): query = AdminUnit.query + if not include_unverified: + query = query.filter(AdminUnit.is_verified) + if keyword: like_keyword = "%" + keyword + "%" keyword_filter = or_( diff --git a/project/static/vue/common/typeahead.vue.js b/project/static/vue/common/typeahead.vue.js index c030369..626c6b7 100644 --- a/project/static/vue/common/typeahead.vue.js +++ b/project/static/vue/common/typeahead.vue.js @@ -62,12 +62,12 @@ const CustomTypeahead = { fetchData(query) { const vm = this axios - .get(this.fetchURL.replace('{query}', escape(query))) + .get(this.fetchURL.replace('{query}', query)) .then(response => { vm.suggestions = response.data.items }) }, - fetchDataDebounced: _.debounce(function(query) { this.fetchData(query) }, 1000), + fetchDataDebounced: _.debounce(function(query) { this.fetchData(query) }, 500), onInput() { this.selected = null; this.fetchDataDebounced(this.query) diff --git a/project/static/vue/organization-organization-invitations/create.vue.js b/project/static/vue/organization-organization-invitations/create.vue.js index 75cbd5a..fb60535 100644 --- a/project/static/vue/organization-organization-invitations/create.vue.js +++ b/project/static/vue/organization-organization-invitations/create.vue.js @@ -17,7 +17,7 @@ const OrganizationOrganizationInvitationCreate = { name="organizationName" v-model="form.organization_name" rules="required|uniqueOrganizationName" - :debounce="1000" /> + :debounce="500" /> + :debounce="500" /> +

{{ $t("shared.models.adminUnit.listName") }}

+ + + + + + + + + + + + + `, + data: () => ({ + errorMessage: null, + fields: [ + { + key: "name", + label: i18n.t("shared.models.adminUnit.name"), + }, + ], + filter: null, + totalRows: 0, + currentPage: 1, + perPage: 10, + searchResult: { + items: [], + }, + }), + methods: { + loadTableData(ctx, callback) { + const vm = this; + axios + .get(`/api/v1/organizations`, { + params: { + page: ctx.currentPage, + per_page: ctx.perPage, + keyword: ctx.filter, + }, + withCredentials: true, + handler: this, + }) + .then((response) => { + vm.totalRows = response.data.total; + callback(response.data.items); + }) + .catch(() => { + callback([]); + }); + return null; + }, + refreshTableData() { + this.$refs.table.refresh(); + }, + handleRequestStart() { + this.errorMessage = null; + }, + handleRequestError(error, message) { + this.errorMessage = message; + }, + }, +}; diff --git a/project/static/vue/organization/read.vue.js b/project/static/vue/organization/read.vue.js new file mode 100644 index 0000000..21f8967 --- /dev/null +++ b/project/static/vue/organization/read.vue.js @@ -0,0 +1,83 @@ +const OrganizationRead = { + template: ` +
+ +
+ + +
+ +
+
+ +

{{ organization.name }}

+
@{{ organization.short_name }}
+ + + + + + + +
+ + {{ organization.fax }} +
+ +
+ + + {{ organization.location.postalCode }} {{ organization.location.city }} +
+
+
+ +
+ +
+
+
+
+ `, + data: () => ({ + isLoading: false, + organization: null, + }), + computed: { + organizationId() { + return this.$route.params.organization_id; + }, + }, + mounted() { + this.isLoading = false; + this.organization = null; + this.loadData(); + }, + methods: { + loadData() { + axios + .get(`/api/v1/organizations/${this.organizationId}`, { + withCredentials: true, + handleLoading: this.handleLoading, + }) + .then((response) => { + this.organization = response.data; + Vue.nextTick(function () { + iFrameResize({ minHeight: 300 }, '#oveda-widget'); + }); + }); + }, + handleLoading(isLoading) { + this.isLoading = isLoading; + }, + }, +}; diff --git a/project/templates/_macros.html b/project/templates/_macros.html index 1ab54e8..8b2045d 100644 --- a/project/templates/_macros.html +++ b/project/templates/_macros.html @@ -681,7 +681,10 @@ {% endif %}
-
{{ event.admin_unit.name }}{{ render_admin_unit_badges(event.admin_unit) }}
+
+ {{ event.admin_unit.name }} + {{ render_admin_unit_badges(event.admin_unit) }} +
{{ render_link_prop(event.admin_unit.url) }} {{ render_email_prop(event.admin_unit.email) }} @@ -1552,40 +1555,44 @@ $('#allday').on('change', function() { $("#name").rules("add", { remote: { - url: "{{ url_for('js_check_org_name') }}", - type: "post" - {% if admin_unit %} - ,data: { - admin_unit_id: function() { - return "{{ admin_unit.id }}"; + param: { + url: "{{ url_for('js_check_org_name') }}", + type: "post" + {% if admin_unit %} + ,data: { + admin_unit_id: function() { + return "{{ admin_unit.id }}"; + } } + {% endif %} } - {% if admin_unit.name %} - ,depends: function() { - return $('#name').val() != '{{ admin_unit.name }}'; + {% if admin_unit and admin_unit.name %} + ,depends: function(element) { + return $('#name').val() !== '{{ admin_unit.name }}'; } {% endif %} - {% endif %} } }); $("#short_name").rules("add", "shortName"); $("#short_name").rules("add", { remote: { - url: "{{ url_for('js_check_org_short_name') }}", - type: "post" - {% if admin_unit %} - ,data: { - admin_unit_id: function() { - return "{{ admin_unit.id }}"; + param: { + url: "{{ url_for('js_check_org_short_name') }}", + type: "post" + {% if admin_unit %} + ,data: { + admin_unit_id: function() { + return "{{ admin_unit.id }}"; + } } + {% endif %} } {% if admin_unit.short_name %} - ,depends: function() { - return $('#short_name').val() != '{{ admin_unit.short_name }}'; + ,depends: function(element) { + return $('#short_name').val() !== '{{ admin_unit.short_name }}'; } {% endif %} - {% endif %} } }); diff --git a/project/templates/example.html b/project/templates/example.html deleted file mode 100644 index cb1afe1..0000000 --- a/project/templates/example.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "layout.html" %} -{%- block title -%} -oveda - Terminkalender für Goslar und Hahnenklee -{%- endblock -%} -{% block content %} -{% block header_before_site_js %} - - -{%- endblock -%} - -

Terminkalender für Goslar und Hahnenklee

- - -{% endblock %} \ No newline at end of file diff --git a/project/templates/layout.html b/project/templates/layout.html index 6b72797..c06738c 100644 --- a/project/templates/layout.html +++ b/project/templates/layout.html @@ -173,8 +173,8 @@ diff --git a/project/templates/layout_vue.html b/project/templates/layout_vue.html index f78a2ec..29fbab7 100644 --- a/project/templates/layout_vue.html +++ b/project/templates/layout_vue.html @@ -52,6 +52,12 @@ en: { shared: { models: { + adminUnit: { + className: "Organization", + listName: "Organizations", + name: "Name", + shortName: "Short name", + }, adminUnitRelation: { targetOrganization: "Other organization", autoVerifyEventReferenceRequests: "Verify reference requests automatically", @@ -132,6 +138,12 @@ de: { shared: { models: { + adminUnit: { + className: "Organisation", + listName: "Organisationen", + name: "Name", + shortName: "Kurzname", + }, adminUnitRelation: { targetOrganization: "Andere Organisation", autoVerifyEventReferenceRequests: diff --git a/project/templates/organization/main.html b/project/templates/organization/main.html new file mode 100644 index 0000000..07ae9c6 --- /dev/null +++ b/project/templates/organization/main.html @@ -0,0 +1,34 @@ +{% extends "layout_vue.html" %} + +{%- block title -%} +{{ _('Organizations') }} +{%- endblock -%} + +{% block header_before_site_js %} +{{ super() }} + +{%- endblock -%} + +{% block component_scripts %} + + +{% endblock %} + +{% block component_definitions %} +Vue.component("OrganizationList", OrganizationList); +Vue.component("OrganizationRead", OrganizationRead); +{% endblock %} + +{% block vue_routes %} +const routes = [ + { + path: "/organizations", + component: OrganizationList, + }, + { + path: "/organizations/:organization_id", + component: OrganizationRead, + name: "OrganizationById", + }, +]; +{% endblock %} diff --git a/project/views/organization.py b/project/views/organization.py new file mode 100644 index 0000000..52c2855 --- /dev/null +++ b/project/views/organization.py @@ -0,0 +1,9 @@ +from flask import render_template + +from project import app + + +@app.route("/organizations") +@app.route("/organizations/") +def organizations(path=None): + return render_template("organization/main.html") diff --git a/project/views/root.py b/project/views/root.py index 14f19f6..4dd80a1 100644 --- a/project/views/root.py +++ b/project/views/root.py @@ -32,11 +32,6 @@ def home(): ) -@app.route("/example") -def example(): - return render_template("example.html") - - @app.route("/tos") def tos(): title = gettext("Terms of service") diff --git a/tests/api/test_event_date.py b/tests/api/test_event_date.py index 4fa6e2b..0920000 100644 --- a/tests/api/test_event_date.py +++ b/tests/api/test_event_date.py @@ -41,8 +41,11 @@ def test_list(client, seeder, utils): def test_search(client, seeder, utils): + from project.dateutils import create_berlin_date + user_id, admin_unit_id = seeder.setup_base() - event_id = seeder.create_event(admin_unit_id) + start = create_berlin_date(2020, 10, 3, 10) + event_id = seeder.create_event(admin_unit_id, start=start) seeder.create_event(admin_unit_id, draft=True) seeder.create_event_unverified() @@ -50,7 +53,7 @@ def test_search(client, seeder, utils): response = utils.get_ok(url) assert len(response.json["items"]) == 1 assert response.json["items"][0]["event"]["id"] == event_id - assert response.json["items"][0]["start"].endswith("+02:00") + assert response.json["items"][0]["start"] == "2020-10-03T10:00:00+02:00" url = utils.get_url("api_v1_event_date_search", keyword="name") response = utils.get_ok(url) diff --git a/tests/api/test_organization.py b/tests/api/test_organization.py index 9a4a2da..c160295 100644 --- a/tests/api/test_organization.py +++ b/tests/api/test_organization.py @@ -15,12 +15,61 @@ def test_read(client, seeder, utils): def test_list(client, seeder, utils): - user_id, admin_unit_id = seeder.setup_base() - + seeder.setup_base() url = utils.get_url("api_v1_organization_list", keyword="crew") utils.get_ok(url) +def test_list_unverified(client, app, seeder, utils): + _, verified_admin_unit_id = seeder.setup_base( + email="verified@test.de", + log_in=False, + name="Verified", + admin_unit_verified=True, + ) + _, unverified_admin_unit_id = seeder.setup_base( + email="unverified@test.de", + log_in=False, + name="Unverified", + admin_unit_verified=False, + ) + + # Unauthorisierte Nutzer sehen nur verifizierte Organisationen + url = utils.get_url("api_v1_organization_list") + response = utils.get_ok(url) + assert len(response.json["items"]) == 1 + assert response.json["items"][0]["id"] == verified_admin_unit_id + + # user_id1 sieht verified_admin_unit_id, weil sie verifiziert ist, + # aber nicht unverified_admin_unit_id, weil sie nicht verifiziert ist. + utils.login("verified@test.de") + response = utils.get_ok(url) + assert len(response.json["items"]) == 1 + assert response.json["items"][0]["id"] == verified_admin_unit_id + + # Authorisierte Nutzer, die Organisationen verifizieren dürfen, sehen alle Organisationen. + # "admin@oveda.de" ist Mitglied der Organisation "Oveda", die andere Organisationen verifizieren darf. + with app.app_context(): + from project.services.admin_unit import get_admin_unit_by_name + + oveda_id = get_admin_unit_by_name("Oveda").id + + utils.logout() + utils.login("admin@oveda.de") + response = utils.get_ok(url) + assert len(response.json["items"]) == 3 + assert response.json["items"][0]["id"] == oveda_id + assert response.json["items"][1]["id"] == unverified_admin_unit_id + assert response.json["items"][2]["id"] == verified_admin_unit_id + + # Globale Admins dürfen alle Organisationen sehen + seeder.create_user("admin@test.de", admin=True) + utils.logout() + utils.login("admin@test.de") + response = utils.get_ok(url) + assert len(response.json["items"]) == 3 + + def test_event_date_search(client, seeder, utils): user_id, admin_unit_id = seeder.setup_base(log_in=False) event_id = seeder.create_event(admin_unit_id) diff --git a/tests/seeder.py b/tests/seeder.py index bc58838..d420da0 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -4,11 +4,20 @@ class Seeder(object): self._db = db self._utils = utils - def setup_base(self, admin=False, log_in=True, admin_unit_verified=True): - user_id = self.create_user(admin=admin) + def setup_base( + self, + admin=False, + log_in=True, + admin_unit_verified=True, + email="test@test.de", + name="Meine Crew", + ): + user_id = self.create_user(email=email, admin=admin) if log_in: self._utils.login() - admin_unit_id = self.create_admin_unit(user_id, verified=admin_unit_verified) + admin_unit_id = self.create_admin_unit( + user_id, name=name, verified=admin_unit_verified + ) return (user_id, admin_unit_id) def setup_base_event_verifier(self): @@ -86,7 +95,7 @@ class Seeder(object): if other_admin_unit: other_admin_unit_id = other_admin_unit.id else: - other_user_id = self.create_user("other@test.de") + other_user_id = self.create_user("admin@oveda.de") other_admin_unit_id = self.create_admin_unit( other_user_id, "Oveda", can_verify_other=True ) diff --git a/tests/views/test_root.py b/tests/views/test_root.py index cfa7c19..778e786 100644 --- a/tests/views/test_root.py +++ b/tests/views/test_root.py @@ -12,8 +12,8 @@ def test_home(client, seeder, utils): utils.assert_response_redirect(response, "home") -def test_example(client, seeder, utils): - url = utils.get_url("example") +def test_organizations(client, seeder, utils): + url = utils.get_url("organizations") utils.get_ok(url)