mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Veranstaltung melden #290
This commit is contained in:
parent
efaa666e66
commit
9696a1dee8
@ -1,3 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
project/cli/test.py
|
||||
project/cli/test.py
|
||||
project/templates/email/*
|
||||
@ -77,7 +77,6 @@ describe("Event", () => {
|
||||
});
|
||||
|
||||
it("read and actions", () => {
|
||||
cy.login();
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
cy.createEvent(adminUnitId).then(function (eventId) {
|
||||
cy.visit("/event/" + eventId);
|
||||
@ -89,6 +88,23 @@ describe("Event", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("report", () => {
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
cy.createEvent(adminUnitId).then(function (eventId) {
|
||||
cy.visit("/event/" + eventId + "/report");
|
||||
|
||||
cy.get("input[name=contactName]").type("Firstname Lastname");
|
||||
cy.get("input[name=contactEmail]").type("firstname.lastname@test.de");
|
||||
cy.get("textarea[name=message]").type("Die Veranstaltung kann leider nicht stattfinden.");
|
||||
cy.screenshot("report");
|
||||
cy.get("button[type=submit]").click();
|
||||
|
||||
cy.get('button[type=submit]').should('not.exist');
|
||||
cy.screenshot("report-submitted");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes", () => {
|
||||
cy.login();
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
|
||||
@ -7,7 +7,7 @@ from flask_security.utils import FsPermNeed
|
||||
from sqlalchemy import and_
|
||||
|
||||
from project import app
|
||||
from project.models import AdminUnit, AdminUnitMember, Event, PublicStatus
|
||||
from project.models import AdminUnit, AdminUnitMember, Event, PublicStatus, User
|
||||
from project.services.admin_unit import get_member_for_admin_unit_by_user_id
|
||||
|
||||
|
||||
@ -206,3 +206,17 @@ def can_read_event_or_401(event: Event):
|
||||
|
||||
def can_read_private_events(admin_unit: AdminUnit) -> bool:
|
||||
return has_access(admin_unit, "event:read")
|
||||
|
||||
|
||||
def get_admin_unit_members_with_permission(admin_unit_id: int, permission: str) -> list:
|
||||
members = (
|
||||
AdminUnitMember.query.join(User)
|
||||
.filter(AdminUnitMember.admin_unit_id == admin_unit_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
return list(
|
||||
filter(
|
||||
lambda member: has_admin_unit_member_permission(member, permission), members
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from flask import make_response
|
||||
from flask import make_response, request
|
||||
from flask_apispec import doc, marshal_with, use_kwargs
|
||||
from sqlalchemy.orm import lazyload, load_only
|
||||
|
||||
@ -10,12 +10,13 @@ from project.access import (
|
||||
login_api_user,
|
||||
login_api_user_or_401,
|
||||
)
|
||||
from project.api import add_api_resource
|
||||
from project.api import add_api_resource, rest_api
|
||||
from project.api.event.schemas import (
|
||||
EventListRequestSchema,
|
||||
EventListResponseSchema,
|
||||
EventPatchRequestSchema,
|
||||
EventPostRequestSchema,
|
||||
EventReportPostSchema,
|
||||
EventSchema,
|
||||
EventSearchRequestSchema,
|
||||
EventSearchResponseSchema,
|
||||
@ -25,6 +26,7 @@ from project.api.event_date.schemas import (
|
||||
EventDateListResponseSchema,
|
||||
)
|
||||
from project.api.resources import BaseResource
|
||||
from project.api.schemas import NoneSchema
|
||||
from project.models import AdminUnit, Event, EventDate, PublicStatus
|
||||
from project.oauth2 import require_oauth
|
||||
from project.services.event import (
|
||||
@ -34,7 +36,10 @@ from project.services.event import (
|
||||
update_event,
|
||||
)
|
||||
from project.services.event_search import EventSearchParams
|
||||
from project.views.event import send_referenced_event_changed_mails
|
||||
from project.views.event import (
|
||||
send_event_report_mails,
|
||||
send_referenced_event_changed_mails,
|
||||
)
|
||||
|
||||
|
||||
def api_can_read_event_or_401(event: Event):
|
||||
@ -154,7 +159,23 @@ class EventSearchResource(BaseResource):
|
||||
return pagination
|
||||
|
||||
|
||||
class EventReportsResource(BaseResource):
|
||||
@doc(summary="Add event report", tags=["Events"])
|
||||
@use_kwargs(EventReportPostSchema, location="json", apply=False)
|
||||
@marshal_with(NoneSchema, 204)
|
||||
def post(self, id):
|
||||
event = Event.query.options(
|
||||
load_only(Event.id, Event.public_status)
|
||||
).get_or_404(id)
|
||||
api_can_read_event_or_401(event)
|
||||
send_event_report_mails(event, request.json)
|
||||
return make_response("", 204)
|
||||
|
||||
|
||||
add_api_resource(EventListResource, "/events", "api_v1_event_list")
|
||||
add_api_resource(EventResource, "/events/<int:id>", "api_v1_event")
|
||||
add_api_resource(EventDatesResource, "/events/<int:id>/dates", "api_v1_event_dates")
|
||||
add_api_resource(EventSearchResource, "/events/search", "api_v1_event_search")
|
||||
rest_api.add_resource(
|
||||
EventReportsResource, "/events/<int:id>/reports", endpoint="api_v1_event_reports"
|
||||
)
|
||||
|
||||
@ -295,3 +295,17 @@ class EventPatchRequestSchema(
|
||||
self.make_patch_schema()
|
||||
|
||||
photo = Owned(ImagePatchRequestSchema)
|
||||
|
||||
|
||||
class EventReportPostSchema(marshmallow.Schema):
|
||||
contact_name = fields.Str(
|
||||
required=True,
|
||||
validate=validate.Length(min=5, max=255),
|
||||
)
|
||||
contact_email = fields.Email(
|
||||
required=True, validate=[validate.Email(), validate.Length(max=255)]
|
||||
)
|
||||
message = fields.Str(
|
||||
required=True,
|
||||
validate=validate.Length(min=20, max=1000),
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from flask_security import hash_password
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from project import user_datastore
|
||||
from project.models import Role, User
|
||||
|
||||
|
||||
def create_user(email, password):
|
||||
@ -52,3 +54,9 @@ def find_user_by_email(email):
|
||||
|
||||
def get_user(id):
|
||||
return user_datastore.find_user(id=id)
|
||||
|
||||
|
||||
def find_all_users_with_role(role_name: str) -> list:
|
||||
return (
|
||||
User.query.options(joinedload(User.roles)).filter(Role.name == role_name).all()
|
||||
)
|
||||
|
||||
53
project/static/vue/common/validated-input.vue.js
Normal file
53
project/static/vue/common/validated-input.vue.js
Normal file
@ -0,0 +1,53 @@
|
||||
const ValidatedInput = {
|
||||
template: `
|
||||
<div>
|
||||
<ValidationProvider :vid="vid" :name="$attrs.label" :rules="rules" v-slot="validationContext">
|
||||
<b-form-group v-bind="$attrs">
|
||||
<b-form-input
|
||||
v-model="innerValue"
|
||||
v-bind="$attrs"
|
||||
:state="getValidationState(validationContext)"
|
||||
></b-form-input>
|
||||
<b-form-invalid-feedback :state="getValidationState(validationContext)">
|
||||
{{ validationContext.errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</ValidationProvider>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
vid: {
|
||||
type: String
|
||||
},
|
||||
rules: {
|
||||
type: [Object, String],
|
||||
default: ""
|
||||
},
|
||||
value: {
|
||||
type: null
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
innerValue: ""
|
||||
}),
|
||||
watch: {
|
||||
// Handles internal model changes.
|
||||
innerValue(newVal) {
|
||||
this.$emit("input", newVal);
|
||||
},
|
||||
// Handles external model changes.
|
||||
value(newVal) {
|
||||
this.innerValue = newVal;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.value) {
|
||||
this.innerValue = this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
53
project/static/vue/common/validated-textarea.vue.js
Normal file
53
project/static/vue/common/validated-textarea.vue.js
Normal file
@ -0,0 +1,53 @@
|
||||
const ValidatedTextarea = {
|
||||
template: `
|
||||
<div>
|
||||
<ValidationProvider :vid="vid" :name="$attrs.label" :rules="rules" v-slot="validationContext">
|
||||
<b-form-group v-bind="$attrs">
|
||||
<b-form-textarea
|
||||
v-model="innerValue"
|
||||
v-bind="$attrs"
|
||||
:state="getValidationState(validationContext)"
|
||||
></b-form-textarea>
|
||||
<b-form-invalid-feedback :state="getValidationState(validationContext)">
|
||||
{{ validationContext.errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</ValidationProvider>
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
vid: {
|
||||
type: String
|
||||
},
|
||||
rules: {
|
||||
type: [Object, String],
|
||||
default: ""
|
||||
},
|
||||
value: {
|
||||
type: null
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
innerValue: ""
|
||||
}),
|
||||
watch: {
|
||||
// Handles internal model changes.
|
||||
innerValue(newVal) {
|
||||
this.$emit("input", newVal);
|
||||
},
|
||||
// Handles external model changes.
|
||||
value(newVal) {
|
||||
this.innerValue = newVal;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.value) {
|
||||
this.innerValue = this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
116
project/static/vue/event-report/create.vue.js
Normal file
116
project/static/vue/event-report/create.vue.js
Normal file
@ -0,0 +1,116 @@
|
||||
const EventReportCreate = {
|
||||
template: `
|
||||
<div>
|
||||
<h1>{{ $t("comp.title") }}</h1>
|
||||
<b-overlay :show="isLoading">
|
||||
<div>
|
||||
<h2 v-if="event"><b-link :href="eventUrl">{{ event.name }}</b-link></h2>
|
||||
<ValidationObserver v-slot="{ handleSubmit }" v-if="!isSubmitted">
|
||||
<b-form @submit.stop.prevent="handleSubmit(submitForm)">
|
||||
<validated-input
|
||||
:label="$t('shared.models.eventReport.contactName')"
|
||||
:description="$t('shared.models.eventReport.contactNameDescription')"
|
||||
name="contactName"
|
||||
v-model="form.contactName"
|
||||
rules="required|min:5" />
|
||||
<validated-input
|
||||
:label="$t('shared.models.eventReport.contactEmail')"
|
||||
name="contactEmail"
|
||||
v-model="form.contactEmail"
|
||||
rules="required|email" />
|
||||
<validated-textarea
|
||||
:label="$t('shared.models.eventReport.message')"
|
||||
:description="$t('shared.models.eventReport.messageDescription')"
|
||||
name="message"
|
||||
v-model="form.message"
|
||||
rules="required|min:20" />
|
||||
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
|
||||
<b-spinner small v-if="isSubmitting"></b-spinner>
|
||||
{{ $t("shared.submit") }}
|
||||
</b-button>
|
||||
</b-form>
|
||||
</ValidationObserver>
|
||||
</div>
|
||||
</b-overlay>
|
||||
</div>
|
||||
`,
|
||||
i18n: {
|
||||
messages: {
|
||||
en: {
|
||||
comp: {
|
||||
title: "Report event",
|
||||
successMessage: "Event successfully reported",
|
||||
},
|
||||
},
|
||||
de: {
|
||||
comp: {
|
||||
title: "Veranstaltung melden",
|
||||
successMessage: "Veranstaltung erfolgreich gemeldet",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
isSubmitted: false,
|
||||
event: null,
|
||||
form: {
|
||||
contactName: "",
|
||||
contactEmail: "",
|
||||
message: "",
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
eventId() {
|
||||
return this.$route.params.event_id;
|
||||
},
|
||||
eventUrl() {
|
||||
return `/event/${this.eventId}`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isLoading = false;
|
||||
this.event = null;
|
||||
this.form = {
|
||||
contactName: "",
|
||||
contactEmail: "",
|
||||
message: "",
|
||||
};
|
||||
this.isSubmitted = false;
|
||||
this.loadFormData();
|
||||
},
|
||||
methods: {
|
||||
loadFormData() {
|
||||
axios
|
||||
.get(`/api/v1/events/${this.eventId}`, {
|
||||
handleLoading: this.handleLoading,
|
||||
})
|
||||
.then((response) => {
|
||||
this.event = response.data;
|
||||
});
|
||||
},
|
||||
handleLoading(isLoading) {
|
||||
this.isLoading = isLoading;
|
||||
},
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null;
|
||||
},
|
||||
submitForm() {
|
||||
const data = {
|
||||
name: this.form.name,
|
||||
};
|
||||
axios
|
||||
.post(`/api/v1/events/${this.eventId}/reports`, data, {
|
||||
handleLoading: this.handleSubmitting,
|
||||
})
|
||||
.then(() => {
|
||||
this.$root.makeSuccessToast(this.$t("comp.successMessage"));
|
||||
this.isSubmitted = true;
|
||||
});
|
||||
},
|
||||
handleSubmitting(isLoading) {
|
||||
this.isSubmitting = isLoading;
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -56,6 +56,8 @@ const OrganizationRelationUpdate = {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isLoading = false;
|
||||
this.relation = null;
|
||||
this.form = {
|
||||
auto_verify_event_reference_requests: false,
|
||||
};
|
||||
|
||||
@ -460,6 +460,7 @@
|
||||
</div>
|
||||
<div class="card-footer small">
|
||||
{{ render_audit(event, show_rating) }}
|
||||
<a href="{{ url_for('event_report', event_id=event.id) }}" class="text-dark" target="_blank" rel="noopener noreferrer">{{ _('Report event') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -615,6 +616,7 @@
|
||||
|
||||
<div class="small mt-2">
|
||||
{{ render_audit(event, show_rating) }}
|
||||
<a href="{{ url_for('event_report', event_id=event.id) }}" class="text-dark">{{ _('Report event') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
9
project/templates/email/event_report_notice.html
Normal file
9
project/templates/email/event_report_notice.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "email/layout.html" %}
|
||||
{% from "_macros.html" import render_email_button %}
|
||||
{% block content %}
|
||||
<p>{{ _('There is a new event report.') }}</p>
|
||||
<p>{{ report['contact_name'] }}</p>
|
||||
<p>{{ report['contact_email'] }}</p>
|
||||
<p>{{ report['message'] }}</p>
|
||||
{{ render_email_button(url_for('event', event_id=event.id, _external=True), _('Click here to view the event')) }}
|
||||
{% endblock %}
|
||||
6
project/templates/email/event_report_notice.txt
Normal file
6
project/templates/email/event_report_notice.txt
Normal file
@ -0,0 +1,6 @@
|
||||
{{ _('There is a new event report.') }}
|
||||
{{ _('Click the link below to view the event') }}
|
||||
{{ report['contact_name'] }}
|
||||
{{ report['contact_email'] }}
|
||||
{{ report['message'] }}
|
||||
{{ url_for('event', event_id=event.id, _external=True) }}
|
||||
22
project/templates/event/report.html
Normal file
22
project/templates/event/report.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "layout_vue.html" %}
|
||||
|
||||
{%- block title -%}
|
||||
{{ _('Report event') }}
|
||||
{%- endblock -%}
|
||||
|
||||
{% block component_scripts %}
|
||||
<script src="{{ url_for('static', filename='vue/event-report/create.vue.js')}}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block component_definitions %}
|
||||
Vue.component("EventReportCreate", EventReportCreate);
|
||||
{% endblock %}
|
||||
|
||||
{% block vue_routes %}
|
||||
const routes = [
|
||||
{
|
||||
path: "/event/:event_id/report",
|
||||
component: EventReportCreate,
|
||||
},
|
||||
];
|
||||
{% endblock %}
|
||||
334
project/templates/layout_vue.html
Normal file
334
project/templates/layout_vue.html
Normal file
@ -0,0 +1,334 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block header_before_site_js %}
|
||||
<link
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.css"
|
||||
/>
|
||||
|
||||
{% if False | env_override('FLASK_DEBUG') %}
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
|
||||
{% else %}
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
{% endif %}
|
||||
<script src="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@2.0.0/dist/vue-router.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-i18n@8.25.0/dist/vue-i18n.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@0.21.1/dist/axios.min.js"></script>
|
||||
<script src="https://unpkg.com/lodash@4.17.21/lodash.min.js"></script>
|
||||
<script src="https://unpkg.com/portal-vue@2.1.7/dist/portal-vue.umd.min.js"></script>
|
||||
<link
|
||||
href="https://unpkg.com/vue-typeahead-bootstrap@2.12.0/dist/VueTypeaheadBootstrap.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://unpkg.com/vue-typeahead-bootstrap@2.12.0/dist/VueTypeaheadBootstrap.umd.min.js"></script>
|
||||
<script src="https://unpkg.com/vee-validate@3.4.11/dist/vee-validate.full.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='vue/common/typeahead.vue.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='vue/common/validated-input.vue.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='vue/common/validated-textarea.vue.js')}}"></script>
|
||||
|
||||
{% block component_scripts %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="vue-container"><router-view></router-view></div>
|
||||
<script>
|
||||
Vue.component("vue-typeahead-bootstrap", VueTypeaheadBootstrap);
|
||||
Vue.component("ValidationObserver", VeeValidate.ValidationObserver);
|
||||
Vue.component("ValidationProvider", VeeValidate.ValidationProvider);
|
||||
|
||||
Vue.component("custom-typeahead", CustomTypeahead);
|
||||
Vue.component("validated-input", ValidatedInput);
|
||||
Vue.component("validated-textarea", ValidatedTextarea);
|
||||
|
||||
{% block component_definitions %}
|
||||
{% endblock %}
|
||||
|
||||
const sharedMessages = {
|
||||
en: {
|
||||
shared: {
|
||||
models: {
|
||||
adminUnitRelation: {
|
||||
targetOrganization: "Other organization",
|
||||
autoVerifyEventReferenceRequests: "Verify reference requests automatically",
|
||||
},
|
||||
eventReport: {
|
||||
contactName: "Name",
|
||||
contactEmail: "Email",
|
||||
contactNameDescription: "First and last name",
|
||||
message: "Message",
|
||||
messageDescription: "Briefly describe in your words why the event is objectionable.",
|
||||
},
|
||||
},
|
||||
cancel: "Cancel",
|
||||
submit: "Submit",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
emptyData: "No data available",
|
||||
errors: {
|
||||
uniqueViolation:
|
||||
"An entry with the entered values already exists. Duplicate entries are not allowed.",
|
||||
},
|
||||
toast: {
|
||||
errorTitle: "Error",
|
||||
successTitle: "Success",
|
||||
},
|
||||
autocomplete: {
|
||||
instruction: "Type to search",
|
||||
},
|
||||
validation: {
|
||||
alpha: "The {_field_} field may only contain alphabetic characters",
|
||||
alpha_num:
|
||||
"The {_field_} field may only contain alpha-numeric characters",
|
||||
alpha_dash:
|
||||
"The {_field_} field may contain alpha-numeric characters as well as dashes and underscores",
|
||||
alpha_spaces:
|
||||
"The {_field_} field may only contain alphabetic characters as well as spaces",
|
||||
between: "The {_field_} field must be between {min} and {max}",
|
||||
confirmed: "The {_field_} field confirmation does not match",
|
||||
digits:
|
||||
"The {_field_} field must be numeric and exactly contain {length} digits",
|
||||
dimensions:
|
||||
"The {_field_} field must be {width} pixels by {height} pixels",
|
||||
email: "The {_field_} field must be a valid email",
|
||||
excluded: "The {_field_} field is not a valid value",
|
||||
ext: "The {_field_} field is not a valid file",
|
||||
image: "The {_field_} field must be an image",
|
||||
integer: "The {_field_} field must be an integer",
|
||||
length: "The {_field_} field must be {length} long",
|
||||
max_value: "The {_field_} field must be {max} or less",
|
||||
max: "The {_field_} field may not be greater than {length} characters",
|
||||
mimes: "The {_field_} field must have a valid file type",
|
||||
min_value: "The {_field_} field must be {min} or more",
|
||||
min: "The {_field_} field must be at least {length} characters",
|
||||
numeric: "The {_field_} field may only contain numeric characters",
|
||||
oneOf: "The {_field_} field is not a valid value",
|
||||
regex: "The {_field_} field format is invalid",
|
||||
required_if: "The {_field_} field is required",
|
||||
required: "The {_field_} field is required",
|
||||
size: "The {_field_} field size must be less than {size}KB",
|
||||
double: "The {_field_} field must be a valid decimal",
|
||||
},
|
||||
},
|
||||
},
|
||||
de: {
|
||||
shared: {
|
||||
models: {
|
||||
adminUnitRelation: {
|
||||
targetOrganization: "Andere Organisation",
|
||||
autoVerifyEventReferenceRequests:
|
||||
"Empfehlungsanfragen automatisch verifizieren",
|
||||
},
|
||||
eventReport: {
|
||||
contactName: "Name",
|
||||
contactNameDescription: "Vor- und Nachname",
|
||||
contactEmail: "Email-Adresse",
|
||||
message: "Mitteilung",
|
||||
messageDescription: "Beschreibe kurz in deinen Worten, warum die Veranstaltung zu beanstanden ist.",
|
||||
},
|
||||
},
|
||||
cancel: "Abbrechen",
|
||||
submit: "Senden",
|
||||
edit: "Bearbeiten",
|
||||
delete: "Löschen",
|
||||
emptyData: "Keine Daten vorhanden",
|
||||
errors: {
|
||||
uniqueViolation:
|
||||
"Ein Eintrag mit den eingegebenen Werten existiert bereits. Doppelte Einträge sind nicht erlaubt.",
|
||||
},
|
||||
toast: {
|
||||
errorTitle: "Fehler",
|
||||
successTitle: "Erfolg",
|
||||
},
|
||||
autocomplete: {
|
||||
instruction: "Tippen um zu suchen",
|
||||
},
|
||||
validation: {
|
||||
alpha: "{_field_} darf nur alphabetische Zeichen enthalten",
|
||||
alpha_dash:
|
||||
"{_field_} darf alphanumerische Zeichen sowie Striche und Unterstriche enthalten",
|
||||
alpha_num: "{_field_} darf nur alphanumerische Zeichen enthalten",
|
||||
alpha_spaces:
|
||||
"{_field_} darf nur alphanumerische Zeichen und Leerzeichen enthalten",
|
||||
between: "{_field_} muss zwischen {min} und {max} liegen",
|
||||
confirmed: "Die Bestätigung von {_field_} stimmt nicht überein",
|
||||
digits:
|
||||
"{_field_} muss numerisch sein und exakt {length} Ziffern enthalten",
|
||||
dimensions: "{_field_} muss {width} x {height} Bildpunkte groß sein",
|
||||
email: "{_field_} muss eine gültige E-Mail-Adresse sein",
|
||||
excluded: "{_field_} muss ein gültiger Wert sein",
|
||||
ext: "{_field_} muss eine gültige Datei sein",
|
||||
image: "{_field_} muss eine Grafik sein",
|
||||
oneOf: "{_field_} muss ein gültiger Wert sein",
|
||||
integer: "{_field_} muss eine ganze Zahl sein",
|
||||
length: "Die Länge von {_field_} muss {length} sein",
|
||||
max: "{_field_} darf nicht länger als {length} Zeichen sein",
|
||||
max_value: "{_field_} darf maximal {max} sein",
|
||||
mimes: "{_field_} muss einen gültigen Dateityp haben",
|
||||
min: "{_field_} muss mindestens {length} Zeichen lang sein",
|
||||
min_value: "{_field_} muss mindestens {min} sein",
|
||||
numeric: "{_field_} darf nur numerische Zeichen enthalten",
|
||||
regex: "Das Format von {_field_} ist ungültig",
|
||||
required: "{_field_} ist ein Pflichtfeld",
|
||||
required_if: "{_field_} ist ein Pflichtfeld",
|
||||
size: "{_field_} muss kleiner als {size}KB sein",
|
||||
double: "Das Feld {_field_} muss eine gültige Dezimalzahl sein",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const i18n = new VueI18n({
|
||||
locale: "de",
|
||||
messages: sharedMessages,
|
||||
silentFallbackWarn: true,
|
||||
});
|
||||
|
||||
VeeValidate.configure({
|
||||
defaultMessage: (field, values) => {
|
||||
return i18n.t(`shared.validation.${values._rule_}`, values);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(VeeValidate.Rules).forEach((rule) => {
|
||||
VeeValidate.extend(rule, VeeValidate.Rules[rule]);
|
||||
});
|
||||
|
||||
{% block vue_routes %}
|
||||
{% endblock %}
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes,
|
||||
mode: "history",
|
||||
base: "/",
|
||||
});
|
||||
|
||||
axios.defaults.baseURL = "{{ get_base_url() }}";
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
if (config) {
|
||||
this.app.handleAxiosStart(config);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
this.app.handleAxiosError(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
if (response && response.config) {
|
||||
this.app.handleAxiosFinish(response.config);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
this.app.handleAxiosError(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
var app = new Vue({
|
||||
el: "#vue-container",
|
||||
i18n,
|
||||
router: router,
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
handleAxiosStart(config) {
|
||||
if (
|
||||
config &&
|
||||
config.handler &&
|
||||
config.handler.hasOwnProperty("handleRequestStart")
|
||||
) {
|
||||
config.handler.handleRequestStart();
|
||||
}
|
||||
|
||||
if (
|
||||
config &&
|
||||
config.hasOwnProperty("handleLoading")
|
||||
) {
|
||||
config.handleLoading(true);
|
||||
}
|
||||
},
|
||||
handleAxiosFinish(config) {
|
||||
if (
|
||||
config &&
|
||||
config.handler &&
|
||||
config.handler.hasOwnProperty("handleRequestFinish")
|
||||
) {
|
||||
config.handler.handleRequestFinish();
|
||||
}
|
||||
|
||||
if (
|
||||
config &&
|
||||
config.hasOwnProperty("handleLoading")
|
||||
) {
|
||||
config.handleLoading(false);
|
||||
}
|
||||
},
|
||||
handleAxiosError(error) {
|
||||
if (error && error.config) {
|
||||
this.handleAxiosFinish(error.config);
|
||||
}
|
||||
|
||||
const status = error && error.response && error.response.status;
|
||||
let message = error.message || error;
|
||||
|
||||
if (status == 400) {
|
||||
message =
|
||||
(error &&
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.message) ||
|
||||
error;
|
||||
errorName =
|
||||
error &&
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.name;
|
||||
|
||||
if (errorName == "Unique Violation") {
|
||||
message = this.$t("shared.errors.uniqueViolation");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
error.config &&
|
||||
error.config.handler &&
|
||||
error.config.handler.hasOwnProperty("handleRequestError")
|
||||
) {
|
||||
error.config.handler.handleRequestError(error, message);
|
||||
} else {
|
||||
this.makeErrorToast(message);
|
||||
}
|
||||
},
|
||||
makeErrorToast(message) {
|
||||
this.makeToast(message, "danger", this.$t("shared.toast.errorTitle"));
|
||||
},
|
||||
makeSuccessToast(message) {
|
||||
this.makeToast(
|
||||
message,
|
||||
"success",
|
||||
this.$t("shared.toast.successTitle")
|
||||
);
|
||||
},
|
||||
makeToast(message, variant, title) {
|
||||
this.$bvToast.toast(message, {
|
||||
title: title,
|
||||
variant: variant,
|
||||
toaster: "b-toaster-top-center",
|
||||
noCloseButton: true,
|
||||
solid: true,
|
||||
});
|
||||
},
|
||||
goBack(fallbackPath) {
|
||||
window.history.length > 1 ? this.$router.go(-1) : this.$router.push({ path: fallbackPath })
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,330 +1,34 @@
|
||||
{% extends "layout.html" %}
|
||||
{% extends "layout_vue.html" %}
|
||||
|
||||
{%- block title -%}
|
||||
{{ _('Relations') }}
|
||||
{%- endblock -%}
|
||||
|
||||
{% block header_before_site_js %}
|
||||
<link
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.css"
|
||||
/>
|
||||
|
||||
{% if False | env_override('FLASK_DEBUG') %}
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
|
||||
{% else %}
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
{% endif %}
|
||||
<script src="https://unpkg.com/bootstrap-vue@2.21.2/dist/bootstrap-vue.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@2.0.0/dist/vue-router.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-i18n@8.25.0/dist/vue-i18n.min.js"></script>
|
||||
<script src="https://unpkg.com/axios@0.21.1/dist/axios.min.js"></script>
|
||||
<script src="https://unpkg.com/lodash@4.17.21/lodash.min.js"></script>
|
||||
<script src="https://unpkg.com/portal-vue@2.1.7/dist/portal-vue.umd.min.js"></script>
|
||||
<link
|
||||
href="https://unpkg.com/vue-typeahead-bootstrap@2.12.0/dist/VueTypeaheadBootstrap.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://unpkg.com/vue-typeahead-bootstrap@2.12.0/dist/VueTypeaheadBootstrap.umd.min.js"></script>
|
||||
<script src="https://unpkg.com/vee-validate@3.4.11/dist/vee-validate.full.min.js"></script>
|
||||
{% block component_scripts %}
|
||||
<script src="{{ url_for('static', filename='vue/organization-relations/list.vue.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='vue/organization-relations/create.vue.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='vue/organization-relations/update.vue.js')}}"></script>
|
||||
<script src="{{ url_for('static', filename='vue/common/typeahead.vue.js')}}"></script>
|
||||
{% endblock %} {% block scripts %} {%- endblock scripts %} {% block content %}
|
||||
<div id="vue-relations"><router-view></router-view></div>
|
||||
<script>
|
||||
Vue.component("vue-typeahead-bootstrap", VueTypeaheadBootstrap);
|
||||
Vue.component("ValidationObserver", VeeValidate.ValidationObserver);
|
||||
Vue.component("ValidationProvider", VeeValidate.ValidationProvider);
|
||||
|
||||
Vue.component("custom-typeahead", CustomTypeahead);
|
||||
Vue.component("OrganizationRelationList", OrganizationRelationList);
|
||||
Vue.component("OrganizationRelationCreate", OrganizationRelationCreate);
|
||||
Vue.component("OrganizationRelationUpdate", OrganizationRelationUpdate);
|
||||
|
||||
const sharedMessages = {
|
||||
en: {
|
||||
shared: {
|
||||
models: {
|
||||
adminUnitRelation: {
|
||||
targetOrganization: "Other organization",
|
||||
autoVerifyEventReferenceRequests: "Verify reference requests automatically",
|
||||
},
|
||||
},
|
||||
cancel: "Cancel",
|
||||
submit: "Submit",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
emptyData: "No data available",
|
||||
errors: {
|
||||
uniqueViolation:
|
||||
"An entry with the entered values already exists. Duplicate entries are not allowed.",
|
||||
},
|
||||
toast: {
|
||||
errorTitle: "Error",
|
||||
successTitle: "Success",
|
||||
},
|
||||
autocomplete: {
|
||||
instruction: "Type to search",
|
||||
},
|
||||
validation: {
|
||||
alpha: "The {_field_} field may only contain alphabetic characters",
|
||||
alpha_num:
|
||||
"The {_field_} field may only contain alpha-numeric characters",
|
||||
alpha_dash:
|
||||
"The {_field_} field may contain alpha-numeric characters as well as dashes and underscores",
|
||||
alpha_spaces:
|
||||
"The {_field_} field may only contain alphabetic characters as well as spaces",
|
||||
between: "The {_field_} field must be between {min} and {max}",
|
||||
confirmed: "The {_field_} field confirmation does not match",
|
||||
digits:
|
||||
"The {_field_} field must be numeric and exactly contain {length} digits",
|
||||
dimensions:
|
||||
"The {_field_} field must be {width} pixels by {height} pixels",
|
||||
email: "The {_field_} field must be a valid email",
|
||||
excluded: "The {_field_} field is not a valid value",
|
||||
ext: "The {_field_} field is not a valid file",
|
||||
image: "The {_field_} field must be an image",
|
||||
integer: "The {_field_} field must be an integer",
|
||||
length: "The {_field_} field must be {length} long",
|
||||
max_value: "The {_field_} field must be {max} or less",
|
||||
max: "The {_field_} field may not be greater than {length} characters",
|
||||
mimes: "The {_field_} field must have a valid file type",
|
||||
min_value: "The {_field_} field must be {min} or more",
|
||||
min: "The {_field_} field must be at least {length} characters",
|
||||
numeric: "The {_field_} field may only contain numeric characters",
|
||||
oneOf: "The {_field_} field is not a valid value",
|
||||
regex: "The {_field_} field format is invalid",
|
||||
required_if: "The {_field_} field is required",
|
||||
required: "The {_field_} field is required",
|
||||
size: "The {_field_} field size must be less than {size}KB",
|
||||
double: "The {_field_} field must be a valid decimal",
|
||||
},
|
||||
},
|
||||
},
|
||||
de: {
|
||||
shared: {
|
||||
models: {
|
||||
adminUnitRelation: {
|
||||
targetOrganization: "Andere Organisation",
|
||||
autoVerifyEventReferenceRequests:
|
||||
"Empfehlungsanfragen automatisch verifizieren",
|
||||
},
|
||||
},
|
||||
cancel: "Abbrechen",
|
||||
submit: "Senden",
|
||||
edit: "Bearbeiten",
|
||||
delete: "Löschen",
|
||||
emptyData: "Keine Daten vorhanden",
|
||||
errors: {
|
||||
uniqueViolation:
|
||||
"Ein Eintrag mit den eingegebenen Werten existiert bereits. Doppelte Einträge sind nicht erlaubt.",
|
||||
},
|
||||
toast: {
|
||||
errorTitle: "Fehler",
|
||||
successTitle: "Erfolg",
|
||||
},
|
||||
autocomplete: {
|
||||
instruction: "Tippen um zu suchen",
|
||||
},
|
||||
validation: {
|
||||
alpha: "{_field_} darf nur alphabetische Zeichen enthalten",
|
||||
alpha_dash:
|
||||
"{_field_} darf alphanumerische Zeichen sowie Striche und Unterstriche enthalten",
|
||||
alpha_num: "{_field_} darf nur alphanumerische Zeichen enthalten",
|
||||
alpha_spaces:
|
||||
"{_field_} darf nur alphanumerische Zeichen und Leerzeichen enthalten",
|
||||
between: "{_field_} muss zwischen {min} und {max} liegen",
|
||||
confirmed: "Die Bestätigung von {_field_} stimmt nicht überein",
|
||||
digits:
|
||||
"{_field_} muss numerisch sein und exakt {length} Ziffern enthalten",
|
||||
dimensions: "{_field_} muss {width} x {height} Bildpunkte groß sein",
|
||||
email: "{_field_} muss eine gültige E-Mail-Adresse sein",
|
||||
excluded: "{_field_} muss ein gültiger Wert sein",
|
||||
ext: "{_field_} muss eine gültige Datei sein",
|
||||
image: "{_field_} muss eine Grafik sein",
|
||||
oneOf: "{_field_} muss ein gültiger Wert sein",
|
||||
integer: "{_field_} muss eine ganze Zahl sein",
|
||||
length: "Die Länge von {_field_} muss {length} sein",
|
||||
max: "{_field_} darf nicht länger als {length} Zeichen sein",
|
||||
max_value: "{_field_} darf maximal {max} sein",
|
||||
mimes: "{_field_} muss einen gültigen Dateityp haben",
|
||||
min: "{_field_} muss mindestens {length} Zeichen lang sein",
|
||||
min_value: "{_field_} muss mindestens {min} sein",
|
||||
numeric: "{_field_} darf nur numerische Zeichen enthalten",
|
||||
regex: "Das Format von {_field_} ist ungültig",
|
||||
required: "{_field_} ist ein Pflichtfeld",
|
||||
required_if: "{_field_} ist ein Pflichtfeld",
|
||||
size: "{_field_} muss kleiner als {size}KB sein",
|
||||
double: "Das Feld {_field_} muss eine gültige Dezimalzahl sein",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const i18n = new VueI18n({
|
||||
locale: "de",
|
||||
messages: sharedMessages,
|
||||
silentFallbackWarn: true,
|
||||
});
|
||||
|
||||
VeeValidate.configure({
|
||||
defaultMessage: (field, values) => {
|
||||
return i18n.t(`shared.validation.${values._rule_}`, values);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(VeeValidate.Rules).forEach((rule) => {
|
||||
VeeValidate.extend(rule, VeeValidate.Rules[rule]);
|
||||
});
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations",
|
||||
component: OrganizationRelationList,
|
||||
},
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations/create",
|
||||
component: OrganizationRelationCreate,
|
||||
},
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations/:relation_id/update",
|
||||
component: OrganizationRelationUpdate,
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes,
|
||||
mode: "history",
|
||||
base: "/",
|
||||
});
|
||||
|
||||
axios.defaults.baseURL = "{{ get_base_url() }}";
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
if (config) {
|
||||
this.app.handleAxiosStart(config);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
this.app.handleAxiosError(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
if (response && response.config) {
|
||||
this.app.handleAxiosFinish(response.config);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
this.app.handleAxiosError(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
var app = new Vue({
|
||||
el: "#vue-relations",
|
||||
i18n,
|
||||
router: router,
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
handleAxiosStart(config) {
|
||||
if (
|
||||
config &&
|
||||
config.handler &&
|
||||
config.handler.hasOwnProperty("handleRequestStart")
|
||||
) {
|
||||
config.handler.handleRequestStart();
|
||||
}
|
||||
|
||||
if (
|
||||
config &&
|
||||
config.hasOwnProperty("handleLoading")
|
||||
) {
|
||||
config.handleLoading(true);
|
||||
}
|
||||
},
|
||||
handleAxiosFinish(config) {
|
||||
if (
|
||||
config &&
|
||||
config.handler &&
|
||||
config.handler.hasOwnProperty("handleRequestFinish")
|
||||
) {
|
||||
config.handler.handleRequestFinish();
|
||||
}
|
||||
|
||||
if (
|
||||
config &&
|
||||
config.hasOwnProperty("handleLoading")
|
||||
) {
|
||||
config.handleLoading(false);
|
||||
}
|
||||
},
|
||||
handleAxiosError(error) {
|
||||
if (error && error.config) {
|
||||
this.handleAxiosFinish(error.config);
|
||||
}
|
||||
|
||||
const status = error && error.response && error.response.status;
|
||||
let message = error.message || error;
|
||||
|
||||
if (status == 400) {
|
||||
message =
|
||||
(error &&
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.message) ||
|
||||
error;
|
||||
errorName =
|
||||
error &&
|
||||
error.response &&
|
||||
error.response.data &&
|
||||
error.response.data.name;
|
||||
|
||||
if (errorName == "Unique Violation") {
|
||||
message = this.$t("shared.errors.uniqueViolation");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
error.config &&
|
||||
error.config.handler &&
|
||||
error.config.handler.hasOwnProperty("handleRequestError")
|
||||
) {
|
||||
error.config.handler.handleRequestError(error, message);
|
||||
} else {
|
||||
this.makeErrorToast(message);
|
||||
}
|
||||
},
|
||||
makeErrorToast(message) {
|
||||
this.makeToast(message, "danger", this.$t("shared.toast.errorTitle"));
|
||||
},
|
||||
makeSuccessToast(message) {
|
||||
this.makeToast(
|
||||
message,
|
||||
"success",
|
||||
this.$t("shared.toast.successTitle")
|
||||
);
|
||||
},
|
||||
makeToast(message, variant, title) {
|
||||
this.$bvToast.toast(message, {
|
||||
title: title,
|
||||
variant: variant,
|
||||
toaster: "b-toaster-top-center",
|
||||
noCloseButton: true,
|
||||
solid: true,
|
||||
});
|
||||
},
|
||||
goBack(fallbackPath) {
|
||||
window.history.length > 1 ? this.$router.go(-1) : this.$router.push({ path: fallbackPath })
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block component_definitions %}
|
||||
Vue.component("OrganizationRelationList", OrganizationRelationList);
|
||||
Vue.component("OrganizationRelationCreate", OrganizationRelationCreate);
|
||||
Vue.component("OrganizationRelationUpdate", OrganizationRelationUpdate);
|
||||
{% endblock %}
|
||||
|
||||
{% block vue_routes %}
|
||||
const routes = [
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations",
|
||||
component: OrganizationRelationList,
|
||||
},
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations/create",
|
||||
component: OrganizationRelationCreate,
|
||||
},
|
||||
{
|
||||
path: "/manage/admin_unit/:admin_unit_id/relations/:relation_id/update",
|
||||
component: OrganizationRelationUpdate,
|
||||
},
|
||||
];
|
||||
{% endblock %}
|
||||
|
||||
@ -12,15 +12,14 @@ from project.access import (
|
||||
can_read_event_or_401,
|
||||
can_reference_event,
|
||||
can_request_event_reference,
|
||||
get_admin_unit_members_with_permission,
|
||||
has_access,
|
||||
has_admin_unit_member_permission,
|
||||
)
|
||||
from project.dateutils import get_next_full_hour
|
||||
from project.forms.event import CreateEventForm, DeleteEventForm, UpdateEventForm
|
||||
from project.jsonld import DateTimeEncoder, get_sd_for_event_date
|
||||
from project.models import (
|
||||
AdminUnit,
|
||||
AdminUnitMember,
|
||||
Event,
|
||||
EventCategory,
|
||||
EventOrganizer,
|
||||
@ -29,7 +28,6 @@ from project.models import (
|
||||
EventReviewStatus,
|
||||
EventSuggestion,
|
||||
PublicStatus,
|
||||
User,
|
||||
)
|
||||
from project.services.event import (
|
||||
get_event_with_details_or_404,
|
||||
@ -48,7 +46,7 @@ from project.views.utils import (
|
||||
get_share_links,
|
||||
handleSqlError,
|
||||
non_match_for_deletion,
|
||||
send_mail,
|
||||
send_mails,
|
||||
)
|
||||
|
||||
|
||||
@ -96,6 +94,14 @@ def event_actions(event_id):
|
||||
)
|
||||
|
||||
|
||||
@app.route("/event/<int:event_id>/report")
|
||||
def event_report(event_id):
|
||||
event = Event.query.get_or_404(event_id)
|
||||
can_read_event_or_401(event)
|
||||
|
||||
return render_template("event/report.html")
|
||||
|
||||
|
||||
@app.route("/admin_unit/<int:id>/events/create", methods=("GET", "POST"))
|
||||
@auth_required()
|
||||
def event_create_for_admin_unit_id(id):
|
||||
@ -380,18 +386,38 @@ def send_referenced_event_changed_mails(event):
|
||||
for reference in references:
|
||||
|
||||
# Alle Mitglieder der AdminUnit, die das Recht haben, Requests zu verifizieren
|
||||
members = (
|
||||
AdminUnitMember.query.join(User)
|
||||
.filter(AdminUnitMember.admin_unit_id == reference.admin_unit_id)
|
||||
.all()
|
||||
members = get_admin_unit_members_with_permission(
|
||||
reference.admin_unit_id, "reference_request:verify"
|
||||
)
|
||||
emails = list(map(lambda member: member.user.email, members))
|
||||
|
||||
send_mails(
|
||||
emails,
|
||||
gettext("Referenced event changed"),
|
||||
"referenced_event_changed_notice",
|
||||
event=event,
|
||||
reference=reference,
|
||||
)
|
||||
|
||||
for member in members:
|
||||
if has_admin_unit_member_permission(member, "reference_request:verify"):
|
||||
send_mail(
|
||||
member.user.email,
|
||||
gettext("Referenced event changed"),
|
||||
"referenced_event_changed_notice",
|
||||
event=event,
|
||||
reference=reference,
|
||||
)
|
||||
|
||||
def send_event_report_mails(event: Event, report: dict):
|
||||
from project.services.user import find_all_users_with_role
|
||||
|
||||
# Alle Mitglieder der AdminUnit, die das Recht haben, Events zu bearbeiten
|
||||
members = get_admin_unit_members_with_permission(
|
||||
event.admin_unit_id, "event:update"
|
||||
)
|
||||
emails = list(map(lambda member: member.user.email, members))
|
||||
|
||||
# Alle globalen Admins
|
||||
admins = find_all_users_with_role("admin")
|
||||
admin_emails = list(map(lambda admin: admin.email, admins))
|
||||
emails.extend(x for x in admin_emails if x not in emails)
|
||||
|
||||
send_mails(
|
||||
emails,
|
||||
gettext("New event report"),
|
||||
"event_report_notice",
|
||||
event=event,
|
||||
report=report,
|
||||
)
|
||||
|
||||
@ -8,16 +8,14 @@ from project import app, db
|
||||
from project.access import (
|
||||
access_or_401,
|
||||
get_admin_unit_for_manage_or_404,
|
||||
get_admin_unit_members_with_permission,
|
||||
get_admin_units_for_event_reference_request,
|
||||
has_admin_unit_member_permission,
|
||||
)
|
||||
from project.forms.reference_request import CreateEventReferenceRequestForm
|
||||
from project.models import (
|
||||
AdminUnitMember,
|
||||
Event,
|
||||
EventReferenceRequest,
|
||||
EventReferenceRequestReviewStatus,
|
||||
User,
|
||||
)
|
||||
from project.services.admin_unit import get_admin_unit_relation
|
||||
from project.services.reference import (
|
||||
@ -28,7 +26,7 @@ from project.views.utils import (
|
||||
flash_errors,
|
||||
get_pagination_urls,
|
||||
handleSqlError,
|
||||
send_mail,
|
||||
send_mails,
|
||||
)
|
||||
|
||||
|
||||
@ -130,15 +128,12 @@ def send_member_reference_request_verify_mails(
|
||||
admin_unit_id, subject, template, **context
|
||||
):
|
||||
# Benachrichtige alle Mitglieder der AdminUnit, die Requests verifizieren können
|
||||
members = (
|
||||
AdminUnitMember.query.join(User)
|
||||
.filter(AdminUnitMember.admin_unit_id == admin_unit_id)
|
||||
.all()
|
||||
members = get_admin_unit_members_with_permission(
|
||||
admin_unit_id, "reference_request:verify"
|
||||
)
|
||||
emails = list(map(lambda member: member.user.email, members))
|
||||
|
||||
for member in members:
|
||||
if has_admin_unit_member_permission(member, "reference_request:verify"):
|
||||
send_mail(member.user.email, subject, template, **context)
|
||||
send_mails(emails, subject, template, **context)
|
||||
|
||||
|
||||
def send_reference_request_inbox_mails(request):
|
||||
|
||||
@ -4,22 +4,24 @@ from flask_security import auth_required
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from project import app, db
|
||||
from project.access import access_or_401, has_access, has_admin_unit_member_permission
|
||||
from project.access import (
|
||||
access_or_401,
|
||||
get_admin_unit_members_with_permission,
|
||||
has_access,
|
||||
)
|
||||
from project.dateutils import get_today
|
||||
from project.forms.reference_request import ReferenceRequestReviewForm
|
||||
from project.models import (
|
||||
AdminUnitMember,
|
||||
EventDate,
|
||||
EventReferenceRequest,
|
||||
EventReferenceRequestReviewStatus,
|
||||
User,
|
||||
)
|
||||
from project.services.admin_unit import (
|
||||
get_admin_unit_relation,
|
||||
upsert_admin_unit_relation,
|
||||
)
|
||||
from project.services.reference import create_event_reference_for_request
|
||||
from project.views.utils import flash_errors, handleSqlError, send_mail
|
||||
from project.views.utils import flash_errors, handleSqlError, send_mails
|
||||
|
||||
|
||||
@app.route("/reference_request/<int:id>/review", methods=("GET", "POST"))
|
||||
@ -123,17 +125,14 @@ def event_reference_request_review_status(id):
|
||||
|
||||
def send_reference_request_review_status_mails(request):
|
||||
# Benachrichtige alle Mitglieder der AdminUnit, die diesen Request erstellt hatte
|
||||
members = (
|
||||
AdminUnitMember.query.join(User)
|
||||
.filter(AdminUnitMember.admin_unit_id == request.event.admin_unit_id)
|
||||
.all()
|
||||
members = get_admin_unit_members_with_permission(
|
||||
request.event.admin_unit_id, "reference_request:create"
|
||||
)
|
||||
emails = list(map(lambda member: member.user.email, members))
|
||||
|
||||
for member in members:
|
||||
if has_admin_unit_member_permission(member, "reference_request:create"):
|
||||
send_mail(
|
||||
member.user.email,
|
||||
gettext("Event review status updated"),
|
||||
"reference_request_review_status_notice",
|
||||
request=request,
|
||||
)
|
||||
send_mails(
|
||||
emails,
|
||||
gettext("Event review status updated"),
|
||||
"reference_request_review_status_notice",
|
||||
request=request,
|
||||
)
|
||||
|
||||
@ -121,11 +121,17 @@ def send_mail(recipient, subject, template, **context):
|
||||
|
||||
|
||||
def send_mails(recipients, subject, template, **context):
|
||||
if len(recipients) == 0: # pragma: no cover
|
||||
return
|
||||
|
||||
msg = Message(subject)
|
||||
msg.recipients = recipients
|
||||
msg.body = render_template("email/%s.txt" % template, **context)
|
||||
msg.html = render_template("email/%s.html" % template, **context)
|
||||
send_mail_message(msg)
|
||||
|
||||
|
||||
def send_mail_message(msg):
|
||||
if not mail.default_sender:
|
||||
app.logger.info(",".join(msg.recipients))
|
||||
app.logger.info(msg.subject)
|
||||
|
||||
@ -10,20 +10,13 @@ from project import app, db
|
||||
from project.access import (
|
||||
admin_unit_suggestions_enabled_or_404,
|
||||
can_read_event_or_401,
|
||||
has_admin_unit_member_permission,
|
||||
get_admin_unit_members_with_permission,
|
||||
)
|
||||
from project.dateutils import get_next_full_hour
|
||||
from project.forms.event_date import FindEventDateForm
|
||||
from project.forms.event_suggestion import CreateEventSuggestionForm
|
||||
from project.jsonld import DateTimeEncoder, get_sd_for_event_date
|
||||
from project.models import (
|
||||
AdminUnit,
|
||||
AdminUnitMember,
|
||||
EventOrganizer,
|
||||
EventReviewStatus,
|
||||
EventSuggestion,
|
||||
User,
|
||||
)
|
||||
from project.models import AdminUnit, EventOrganizer, EventReviewStatus, EventSuggestion
|
||||
from project.services.event import (
|
||||
get_event_date_with_details_or_404,
|
||||
get_event_dates_query,
|
||||
@ -40,7 +33,7 @@ from project.views.utils import (
|
||||
get_pagination_urls,
|
||||
get_share_links,
|
||||
handleSqlError,
|
||||
send_mail,
|
||||
send_mails,
|
||||
)
|
||||
|
||||
|
||||
@ -211,17 +204,12 @@ def get_styles(admin_unit):
|
||||
|
||||
|
||||
def send_event_inbox_mails(admin_unit, event_suggestion):
|
||||
members = (
|
||||
AdminUnitMember.query.join(User)
|
||||
.filter(AdminUnitMember.admin_unit_id == admin_unit.id)
|
||||
.all()
|
||||
)
|
||||
members = get_admin_unit_members_with_permission(admin_unit.id, "event:verify")
|
||||
emails = list(map(lambda member: member.user.email, members))
|
||||
|
||||
for member in members:
|
||||
if has_admin_unit_member_permission(member, "event:verify"):
|
||||
send_mail(
|
||||
member.user.email,
|
||||
gettext("New event review"),
|
||||
"review_notice",
|
||||
event_suggestion=event_suggestion,
|
||||
)
|
||||
send_mails(
|
||||
emails,
|
||||
gettext("New event review"),
|
||||
"review_notice",
|
||||
event_suggestion=event_suggestion,
|
||||
)
|
||||
|
||||
@ -528,3 +528,31 @@ def test_delete(client, seeder, utils, app):
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event is None
|
||||
|
||||
|
||||
def test_report_mail(client, seeder, utils, app, mocker):
|
||||
user_id, admin_unit_id = seeder.setup_base(admin=False, log_in=False)
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
seeder.create_user(email="admin@test.de", admin=True)
|
||||
|
||||
mail_mock = utils.mock_send_mails(mocker)
|
||||
url = utils.get_url("api_v1_event_reports", id=event_id)
|
||||
response = utils.post_json(
|
||||
url,
|
||||
{
|
||||
"contact_name": "Firstname Lastname",
|
||||
"contact_email": "firstname.lastname@test.de",
|
||||
"message": "Diese Veranstaltung wird nicht stattfinden.",
|
||||
},
|
||||
)
|
||||
|
||||
utils.assert_response_no_content(response)
|
||||
utils.assert_send_mail_called(
|
||||
mail_mock,
|
||||
["test@test.de", "admin@test.de"],
|
||||
[
|
||||
"Firstname Lastname",
|
||||
"firstname.lastname@test.de",
|
||||
"Diese Veranstaltung wird nicht stattfinden.",
|
||||
],
|
||||
)
|
||||
|
||||
@ -175,12 +175,28 @@ class UtilActions(object):
|
||||
)
|
||||
|
||||
def mock_send_mails(self, mocker):
|
||||
return mocker.patch("project.views.utils.send_mails")
|
||||
return mocker.patch("project.views.utils.send_mail_message")
|
||||
|
||||
def assert_send_mail_called(self, mock, recipient):
|
||||
def assert_send_mail_called(self, mock, recipients, contents=None):
|
||||
mock.assert_called_once()
|
||||
args, kwargs = mock.call_args
|
||||
assert args[0] == [recipient]
|
||||
message = args[0]
|
||||
|
||||
if not isinstance(recipients, list):
|
||||
recipients = [recipients]
|
||||
|
||||
for recipient in recipients:
|
||||
assert recipient in message.recipients, f"{recipient} not in recipients"
|
||||
|
||||
if contents:
|
||||
if not isinstance(contents, list):
|
||||
contents = [contents]
|
||||
|
||||
for content in contents:
|
||||
assert content in message.body, f"{content} not in body"
|
||||
assert content in message.html, f"{content} not in html"
|
||||
|
||||
return message
|
||||
|
||||
def mock_now(self, mocker, year, month, day):
|
||||
from project.dateutils import create_berlin_date
|
||||
|
||||
@ -481,3 +481,11 @@ def test_rrule(client, seeder, utils, app):
|
||||
occurence = json["occurrences"][0]
|
||||
assert occurence["date"] == "20201125T000000"
|
||||
assert occurence["formattedDate"] == '"25.11.2020"'
|
||||
|
||||
|
||||
def test_report(seeder, utils):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
url = utils.get_url("event_report", event_id=event_id)
|
||||
utils.get_ok(url)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user