Merge pull request #518 from eventcally/issue/517

Add trackable info to API #517
This commit is contained in:
Daniel Grams 2023-07-09 13:55:41 +02:00 committed by GitHub
commit 79f61b66eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 585 additions and 188 deletions

View File

@ -35,7 +35,7 @@ from project.services.event import (
get_significant_event_changes,
update_event,
)
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
from project.views.event import (
send_event_report_mails,
send_referenced_event_changed_mails,
@ -62,17 +62,20 @@ class EventListResource(BaseResource):
@marshal_with(EventListResponseSchema)
@require_api_access()
def get(self, **kwargs):
pagination = (
Event.query.join(Event.admin_unit)
.filter(
and_(
Event.public_status == PublicStatus.published,
AdminUnit.is_verified,
)
params = EventSearchParams()
params.load_from_request(**kwargs)
query = Event.query.join(Event.admin_unit).filter(
and_(
Event.public_status == PublicStatus.published,
AdminUnit.is_verified,
)
.paginate()
)
return pagination
query = params.get_trackable_query(query, Event)
query = params.get_trackable_order_by(query, Event)
query = query.order_by(Event.min_start)
return query.paginate()
class EventResource(BaseResource):
@ -173,7 +176,7 @@ class EventSearchResource(BaseResource):
def get(self, **kwargs):
login_api_user()
params = EventSearchParams()
params.load_from_request()
params.load_from_request(**kwargs)
pagination = get_events_query(params).paginate()
return pagination

View File

@ -37,6 +37,7 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
WriteIdSchemaMixin,
)
@ -222,7 +223,10 @@ class EventRefSchema(EventIdSchema):
class EventSearchItemSchema(
EventRefSchema, EventCurrentUserMixin, EventCurrentOrganizationMixin
EventRefSchema,
EventCurrentUserMixin,
EventCurrentOrganizationMixin,
TrackableSchemaMixin,
):
description = marshmallow.auto_field()
date_definitions = fields.List(fields.Nested(EventDateDefinitionSchema))
@ -237,27 +241,53 @@ class EventSearchItemSchema(
public_status = EnumField(PublicStatus)
class EventListRequestSchema(PaginationRequestSchema):
class EventListRequestSchema(PaginationRequestSchema, TrackableRequestSchemaMixin):
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
[
"-created_at",
"-updated_at",
"-last_modified_at",
"start",
]
),
)
class EventListRefSchema(EventRefSchema, TrackableSchemaMixin):
pass
class EventListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventRefSchema), metadata={"description": "Events"}
fields.Nested(EventListRefSchema), metadata={"description": "Events"}
)
class UserFavoriteEventListRequestSchema(PaginationRequestSchema):
pass
class UserFavoriteEventListRequestSchema(
PaginationRequestSchema, TrackableRequestSchemaMixin
):
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
[
"-created_at",
"-updated_at",
"-last_modified_at",
"start",
]
),
)
class UserFavoriteEventListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventRefSchema), metadata={"description": "Events"}
fields.Nested(EventListRefSchema), metadata={"description": "Events"}
)
class EventSearchRequestSchema(PaginationRequestSchema):
class EventSearchRequestSchema(PaginationRequestSchema, TrackableRequestSchemaMixin):
keyword = fields.Str(
metadata={"description": "Looks for keyword in name, description and tags."},
)
@ -267,9 +297,7 @@ class EventSearchRequestSchema(PaginationRequestSchema):
},
)
date_to = fields.Date(
metadata={
"description": "Looks for events at or before this date, e.g. 2020-12-31."
},
metadata={"description": "Looks for events before this date, e.g. 2020-12-31."},
)
coordinate = fields.Str(
metadata={
@ -294,11 +322,38 @@ class EventSearchRequestSchema(PaginationRequestSchema):
)
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
[
"-created_at",
"-updated_at",
"-last_modified_at",
"-rating",
"-reference_created_at",
"start",
]
),
)
status = fields.List(
EnumField(EventStatus),
metadata={"description": "Looks for events with this stati."},
)
postal_code = fields.List(
fields.Str(),
metadata={"description": "Looks for events with places with this postal code."},
)
exclude_recurring = fields.Bool(
metadata={"description": "Exclude recurring events"},
)
include_organization_references = fields.Bool(
metadata={
"description": "Include events referenced by organization. Only valid if there is an organization context."
},
)
organization_references_only = fields.Bool(
metadata={
"description": "Only events referenced by organization. Only valid if there is an organization context."
},
)
class EventSearchResponseSchema(PaginationResponseSchema):

View File

@ -16,7 +16,7 @@ from project.api.event_date.schemas import (
from project.api.resources import BaseResource, require_api_access
from project.models import AdminUnit, Event, EventDate, PublicStatus
from project.services.event import get_event_dates_query
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
from project.views.utils import get_current_admin_unit_for_api
@ -26,7 +26,7 @@ class EventDateListResource(BaseResource):
@marshal_with(EventDateListResponseSchema)
@require_api_access()
def get(self, **kwargs):
pagination = (
query = (
EventDate.query.join(EventDate.event)
.join(Event.admin_unit)
.options(lazyload(EventDate.event))
@ -36,9 +36,14 @@ class EventDateListResource(BaseResource):
AdminUnit.is_verified,
)
)
.paginate()
)
return pagination
params = EventSearchParams()
params.load_from_request(**kwargs)
query = params.get_trackable_query(query, Event)
query = params.get_trackable_order_by(query, Event)
query = query.order_by(EventDate.start)
return query.paginate()
class EventDateResource(BaseResource):
@ -63,9 +68,9 @@ class EventDateSearchResource(BaseResource):
def get(self, **kwargs):
login_api_user()
params = EventSearchParams()
params.load_from_request()
params.can_read_planned_events = can_use_planning()
params.include_admin_unit_references = True
params.load_from_request(**kwargs)
params.can_read_planned_events = can_use_planning()
if "not_referenced" in request.args:
admin_unit = get_current_admin_unit_for_api()

View File

@ -1,4 +1,4 @@
from marshmallow import fields
from marshmallow import fields, validate
from project.api import marshmallow
from project.api.event.schemas import (
@ -7,7 +7,12 @@ from project.api.event.schemas import (
EventSearchRequestSchema,
)
from project.api.fields import CustomDateTimeField
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
)
from project.models import EventDate
@ -32,13 +37,27 @@ class EventDateRefSchema(marshmallow.SQLAlchemySchema):
start = CustomDateTimeField()
class EventDateListRequestSchema(PaginationRequestSchema):
class EventDateListRequestSchema(PaginationRequestSchema, TrackableRequestSchemaMixin):
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
[
"-created_at",
"-updated_at",
"-last_modified_at",
"start",
]
),
)
class EventDateListRefSchema(EventDateRefSchema, TrackableSchemaMixin):
pass
class EventDateListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventDateRefSchema), metadata={"description": "Dates"}
fields.Nested(EventDateListRefSchema), metadata={"description": "Dates"}
)

View File

@ -13,7 +13,7 @@ from project.api.event_list.schemas import (
from project.api.resources import BaseResource, require_api_access
from project.models import Event, EventList
from project.services.event import get_events_query
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
class EventListModelResource(BaseResource):

View File

@ -1,4 +1,4 @@
from marshmallow import fields
from marshmallow import fields, validate
from project.api import marshmallow
from project.api.event.schemas import EventRefSchema, EventWriteIdSchema
@ -8,6 +8,8 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
)
from project.models import EventReference
@ -22,7 +24,7 @@ class EventReferenceIdSchema(EventReferenceModelSchema, IdSchemaMixin):
pass
class EventReferenceRefSchema(EventReferenceIdSchema):
class EventReferenceRefSchema(EventReferenceIdSchema, TrackableSchemaMixin):
event = fields.Nested(EventRefSchema)
@ -36,8 +38,13 @@ class EventReferenceDumpSchema(EventReferenceIdSchema):
organization_id = fields.Int(attribute="admin_unit_id")
class EventReferenceListRequestSchema(PaginationRequestSchema):
pass
class EventReferenceListRequestSchema(
PaginationRequestSchema, TrackableRequestSchemaMixin
):
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(["-created_at", "-updated_at", "-last_modified_at"]),
)
class EventReferenceListResponseSchema(PaginationResponseSchema):

View File

@ -1,7 +1,7 @@
from marshmallow import ValidationError, fields
from marshmallow_sqlalchemy import fields as msfields
from project.dateutils import berlin_tz
from project.dateutils import berlin_tz, gmt_tz
class NumericStr(fields.String):
@ -34,6 +34,22 @@ class CustomDateTimeField(fields.DateTime):
return result
class GmtDateTimeField(fields.DateTime):
def _serialize(self, value, attr, obj, **kwargs):
if value:
value = value.replace(tzinfo=gmt_tz)
return super()._serialize(value, attr, obj, **kwargs)
def deserialize(self, value, attr, data, **kwargs):
result = super().deserialize(value, attr, data, **kwargs)
if result and result.tzinfo is None:
result = gmt_tz.localize(result)
return result
class Owned(msfields.Nested):
def _deserialize(self, *args, **kwargs):
if (

View File

@ -77,7 +77,7 @@ from project.api.place.schemas import (
)
from project.api.resources import BaseResource, require_api_access
from project.models import AdminUnit, Event, PublicStatus
from project.models.admin_unit import AdminUnitRelation
from project.models.admin_unit import AdminUnitInvitation, AdminUnitRelation
from project.services.admin_unit import (
get_admin_unit_invitation_query,
get_admin_unit_query,
@ -88,13 +88,20 @@ from project.services.admin_unit import (
get_place_query,
)
from project.services.event import get_event_dates_query, get_events_query, insert_event
from project.services.event_search import EventSearchParams
from project.services.importer.event_importer import EventImporter
from project.services.reference import (
get_reference_incoming_query,
get_reference_outgoing_query,
get_relation_outgoing_query,
)
from project.services.search_params import (
AdminUnitSearchParams,
EventPlaceSearchParams,
EventReferenceSearchParams,
EventSearchParams,
OrganizerSearchParams,
TrackableSearchParams,
)
from project.views.utils import get_current_admin_unit_for_api, send_mail_async
@ -119,10 +126,10 @@ class OrganizationEventDateSearchResource(BaseResource):
admin_unit = AdminUnit.query.get_or_404(id)
params = EventSearchParams()
params.load_from_request()
params.include_admin_unit_references = True
params.load_from_request(**kwargs)
params.admin_unit_id = admin_unit.id
params.can_read_private_events = api_can_read_private_events(admin_unit)
params.include_admin_unit_references = True
pagination = get_event_dates_query(params).paginate()
return pagination
@ -137,7 +144,7 @@ class OrganizationEventSearchResource(BaseResource):
admin_unit = AdminUnit.query.get_or_404(id)
params = EventSearchParams()
params.load_from_request()
params.load_from_request(**kwargs)
params.admin_unit_id = admin_unit.id
params.can_read_private_events = api_can_read_private_events(admin_unit)
@ -151,8 +158,10 @@ class OrganizationEventListResource(BaseResource):
@marshal_with(EventListResponseSchema)
@require_api_access()
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
params = EventSearchParams()
params.load_from_request(**kwargs)
admin_unit = AdminUnit.query.get_or_404(id)
event_filter = Event.admin_unit_id == admin_unit.id
if not api_can_read_private_events(admin_unit):
@ -162,8 +171,12 @@ class OrganizationEventListResource(BaseResource):
AdminUnit.is_verified,
)
pagination = Event.query.join(Event.admin_unit).filter(event_filter).paginate()
return pagination
query = Event.query.join(Event.admin_unit).filter(event_filter)
query = params.get_trackable_query(query, Event)
query = params.get_trackable_order_by(query, Event)
query = query.order_by(Event.min_start)
return query.paginate()
@doc(
summary="Add new event",
@ -223,8 +236,6 @@ class OrganizationListResource(BaseResource):
@marshal_with(OrganizationListResponseSchema)
@require_api_access()
def get(self, **kwargs):
keyword = kwargs["keyword"] if "keyword" in kwargs else None
login_api_user()
include_unverified = can_verify_admin_unit()
reference_request_for_admin_unit_id = None
@ -235,11 +246,11 @@ class OrganizationListResource(BaseResource):
if admin_unit:
reference_request_for_admin_unit_id = admin_unit.id
pagination = get_admin_unit_query(
keyword,
include_unverified,
reference_request_for_admin_unit_id=reference_request_for_admin_unit_id,
).paginate()
params = AdminUnitSearchParams()
params.load_from_request(**kwargs)
params.include_unverified = include_unverified
params.reference_request_for_admin_unit_id = reference_request_for_admin_unit_id
pagination = get_admin_unit_query(params).paginate()
return pagination
@ -252,9 +263,11 @@ class OrganizationOrganizerListResource(BaseResource):
@require_api_access()
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
name = kwargs["name"] if "name" in kwargs else None
pagination = get_organizer_query(admin_unit.id, name).paginate()
params = OrganizerSearchParams()
params.load_from_request(**kwargs)
params.admin_unit_id = admin_unit.id
pagination = get_organizer_query(params).paginate()
return pagination
@doc(
@ -285,9 +298,11 @@ class OrganizationPlaceListResource(BaseResource):
@require_api_access()
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
name = kwargs["name"] if "name" in kwargs else None
pagination = get_place_query(admin_unit.id, name).paginate()
params = EventPlaceSearchParams()
params.load_from_request(**kwargs)
params.admin_unit_id = admin_unit.id
pagination = get_place_query(params).paginate()
return pagination
@doc(
@ -322,7 +337,10 @@ class OrganizationIncomingEventReferenceListResource(BaseResource):
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
pagination = get_reference_incoming_query(admin_unit).paginate()
params = EventReferenceSearchParams()
params.load_from_request(**kwargs)
params.admin_unit_id = admin_unit.id
pagination = get_reference_incoming_query(params).paginate()
return pagination
@doc(
@ -430,8 +448,13 @@ class OrganizationOrganizationInvitationListResource(BaseResource):
admin_unit = get_admin_unit_for_manage_or_404(id)
access_or_401(admin_unit, "admin_unit:update")
pagination = get_admin_unit_invitation_query(admin_unit).paginate()
return pagination
params = TrackableSearchParams()
params.load_from_request(**kwargs)
query = get_admin_unit_invitation_query(admin_unit)
query = params.get_trackable_query(query, AdminUnitInvitation)
query = params.get_trackable_order_by(query, AdminUnitInvitation)
return query.paginate()
@doc(
summary="Add new organization invitation",

View File

@ -1,4 +1,4 @@
from marshmallow import fields, post_dump
from marshmallow import fields, post_dump, validate
from project.access import has_access, login_api_user
from project.api import marshmallow
@ -9,6 +9,8 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
WriteIdSchemaMixin,
)
from project.models import AdminUnit
@ -66,15 +68,23 @@ class OrganizationRefSchema(OrganizationIdSchema):
name = marshmallow.auto_field()
class OrganizationListRefSchema(OrganizationRefSchema):
class OrganizationListRefSchema(OrganizationRefSchema, TrackableSchemaMixin):
short_name = marshmallow.auto_field()
is_verified = fields.Boolean()
class OrganizationListRequestSchema(PaginationRequestSchema):
class OrganizationListRequestSchema(
PaginationRequestSchema, TrackableRequestSchemaMixin
):
keyword = fields.Str(
metadata={"description": "Looks for keyword in name and short name."},
)
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
["-created_at", "-updated_at", "-last_modified_at", "name"]
),
)
class OrganizationListResponseSchema(PaginationResponseSchema):

View File

@ -1,4 +1,4 @@
from marshmallow import fields
from marshmallow import fields, validate
from project.api import marshmallow
from project.api.organization.schemas import OrganizationRefSchema
@ -7,6 +7,7 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
)
from project.models import AdminUnitInvitation
@ -40,13 +41,26 @@ class OrganizationInvitationRefSchema(OrganizationInvitationIdSchema):
email = marshmallow.auto_field()
class OrganizationInvitationListRequestSchema(PaginationRequestSchema):
class OrganizationInvitationListRequestSchema(
PaginationRequestSchema, TrackableRequestSchemaMixin
):
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
["-created_at", "-updated_at", "-last_modified_at", "name"]
),
)
class OrganizationInvitationListRefSchema(
OrganizationInvitationRefSchema, TrackableSchemaMixin
):
pass
class OrganizationInvitationListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(OrganizationInvitationRefSchema),
fields.Nested(OrganizationInvitationListRefSchema),
metadata={"description": "Organization invitations"},
)

View File

@ -15,6 +15,7 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
WriteIdSchemaMixin,
)
@ -67,15 +68,25 @@ class OrganizerRefSchema(OrganizerIdSchema):
name = marshmallow.auto_field()
class OrganizerListRequestSchema(PaginationRequestSchema):
class OrganizerListRequestSchema(PaginationRequestSchema, TrackableRequestSchemaMixin):
name = fields.Str(
metadata={"description": "Looks for name."},
)
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
["-created_at", "-updated_at", "-last_modified_at", "name"]
),
)
class OrganizerListRefSchema(OrganizerRefSchema, TrackableSchemaMixin):
pass
class OrganizerListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(OrganizerRefSchema), metadata={"description": "Organizers"}
fields.Nested(OrganizerListRefSchema), metadata={"description": "Organizers"}
)

View File

@ -16,6 +16,7 @@ from project.api.schemas import (
PaginationRequestSchema,
PaginationResponseSchema,
SQLAlchemyBaseSchema,
TrackableRequestSchemaMixin,
TrackableSchemaMixin,
WriteIdSchemaMixin,
)
@ -64,15 +65,25 @@ class PlaceSearchItemSchema(PlaceRefSchema):
location = fields.Nested(LocationSearchItemSchema)
class PlaceListRequestSchema(PaginationRequestSchema):
class PlaceListRequestSchema(PaginationRequestSchema, TrackableRequestSchemaMixin):
name = fields.Str(
metadata={"description": "Looks for name."},
)
sort = fields.Str(
metadata={"description": "Sort result items."},
validate=validate.OneOf(
["-created_at", "-updated_at", "-last_modified_at", "name"]
),
)
class PlaceListRefSchema(PlaceRefSchema, TrackableSchemaMixin):
pass
class PlaceListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(PlaceRefSchema), metadata={"description": "Places"}
fields.Nested(PlaceListRefSchema), metadata={"description": "Places"}
)

View File

@ -2,6 +2,7 @@ from marshmallow import ValidationError, fields, missing, validate
from marshmallow.decorators import pre_load
from project.api import marshmallow
from project.api.fields import GmtDateTimeField
class SQLAlchemyBaseSchema(marshmallow.SQLAlchemySchema):
@ -39,8 +40,21 @@ class WriteIdSchemaMixin(object):
class TrackableSchemaMixin(object):
created_at = marshmallow.auto_field(dump_only=True)
updated_at = marshmallow.auto_field(dump_only=True)
created_at = GmtDateTimeField(dump_only=True)
updated_at = GmtDateTimeField(dump_only=True)
class TrackableRequestSchemaMixin(object):
created_at_from = GmtDateTimeField(
metadata={
"description": "Items created at or after this date time in GTM, e.g. 2020-12-31T00:00:00."
},
)
created_at_to = GmtDateTimeField(
metadata={
"description": "Items created before this date time in GTM, e.g. 2020-12-31T00:00:00."
},
)
class ErrorResponseSchema(marshmallow.Schema):

View File

@ -21,7 +21,7 @@ from project.api.resources import BaseResource, require_api_access
from project.models import AdminUnitInvitation, Event
from project.services.admin_unit import get_admin_unit_organization_invitations_query
from project.services.event import get_events_query
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams, TrackableSearchParams
from project.utils import strings_are_equal_ignoring_case
@ -41,11 +41,14 @@ class UserOrganizationInvitationListResource(BaseResource):
def get(self, **kwargs):
login_api_user_or_401()
pagination = get_admin_unit_organization_invitations_query(
current_user.email
).paginate()
params = TrackableSearchParams()
params.load_from_request(**kwargs)
return pagination
query = get_admin_unit_organization_invitations_query(current_user.email)
query = params.get_trackable_query(query, AdminUnitInvitation)
query = params.get_trackable_order_by(query, AdminUnitInvitation)
return query.paginate()
class UserOrganizationInvitationResource(BaseResource):
@ -92,8 +95,15 @@ class UserFavoriteEventListResource(BaseResource):
login_api_user_or_401()
pagination = get_favorite_events_query(current_user.id).paginate()
return pagination
query = get_favorite_events_query(current_user.id)
params = EventSearchParams()
params.load_from_request(**kwargs)
query = params.get_trackable_query(query, Event)
query = params.get_trackable_order_by(query, Event)
query = query.order_by(Event.min_start)
return query.paginate()
class UserFavoriteEventSearchResource(BaseResource):
@ -108,7 +118,7 @@ class UserFavoriteEventSearchResource(BaseResource):
login_api_user_or_401()
params = EventSearchParams()
params.load_from_request()
params.load_from_request(**kwargs)
params.favored_by_user_id = current_user.id
pagination = get_events_query(params).paginate()

View File

@ -2,7 +2,6 @@ from sqlalchemy import Column, Integer, String, Unicode
from sqlalchemy.orm import deferred
from project import db
from project.dateutils import gmt_tz
from project.models.iowned import IOwned
from project.models.trackable_mixin import TrackableMixin
@ -23,13 +22,6 @@ class Image(db.Model, TrackableMixin, IOwned):
def is_empty(self):
return not self.data
def get_hash(self):
return (
int(self.updated_at.replace(tzinfo=gmt_tz).timestamp() * 1000)
if self.updated_at
else 0
)
def get_file_extension(self):
return self.encoding_format.split("/")[-1] if self.encoding_format else "png"

View File

@ -1,8 +1,10 @@
import datetime
from sqlalchemy import Column, DateTime, ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import backref, declared_attr, deferred, relationship
from project.dateutils import gmt_tz
from project.models.functions import _current_user_id_or_none
@ -18,7 +20,6 @@ class TrackableMixin(object):
return deferred(
Column(
DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
),
group="trackable",
@ -50,7 +51,6 @@ class TrackableMixin(object):
Column(
"updated_by_id",
ForeignKey("user.id", ondelete="SET NULL"),
default=_current_user_id_or_none,
onupdate=_current_user_id_or_none,
),
group="trackable",
@ -64,3 +64,14 @@ class TrackableMixin(object):
remote_side="User.id",
backref=backref("updated_%s" % cls.__tablename__, lazy=True),
)
@hybrid_property
def last_modified_at(self):
return self.updated_at if self.updated_at else self.created_at
def get_hash(self):
return (
int(self.last_modified_at.replace(tzinfo=gmt_tz).timestamp() * 1000)
if self.last_modified_at
else 0
)

View File

@ -24,6 +24,11 @@ from project.services.reference import (
get_newest_reference_requests,
get_newest_references,
)
from project.services.search_params import (
AdminUnitSearchParams,
EventPlaceSearchParams,
OrganizerSearchParams,
)
def insert_admin_unit_for_user(admin_unit, user, invitation=None):
@ -175,59 +180,66 @@ def get_admin_unit_member(id):
return AdminUnitMember.query.filter_by(id=id).first()
def get_admin_unit_query(
keyword=None,
include_unverified=False,
only_verifier=False,
reference_request_for_admin_unit_id=None,
):
def get_admin_unit_query(params: AdminUnitSearchParams):
query = AdminUnit.query
if not include_unverified:
if not params.include_unverified:
query = query.filter(AdminUnit.is_verified)
if only_verifier:
if params.only_verifier:
only_verifier_filter = and_(
AdminUnit.can_verify_other, AdminUnit.incoming_verification_requests_allowed
)
query = query.filter(only_verifier_filter)
if reference_request_for_admin_unit_id:
if params.reference_request_for_admin_unit_id:
request_filter = and_(
AdminUnit.id != reference_request_for_admin_unit_id,
AdminUnit.id != params.reference_request_for_admin_unit_id,
AdminUnit.incoming_reference_requests_allowed,
)
query = query.filter(request_filter)
if keyword:
like_keyword = "%" + keyword + "%"
order_keyword = keyword + "%"
query = params.get_trackable_query(query, AdminUnit)
if params.keyword:
like_keyword = "%" + params.keyword + "%"
order_keyword = params.keyword + "%"
keyword_filter = or_(
AdminUnit.name.ilike(like_keyword),
AdminUnit.short_name.ilike(like_keyword),
)
query = query.filter(keyword_filter).order_by(
query = query.filter(keyword_filter)
query = params.get_trackable_order_by(query, AdminUnit)
query = query.order_by(
AdminUnit.name.ilike(order_keyword).desc(),
AdminUnit.short_name.ilike(order_keyword).desc(),
func.lower(AdminUnit.name),
)
else:
query = params.get_trackable_order_by(query, AdminUnit)
query = query.order_by(func.lower(AdminUnit.name))
return query
def get_organizer_query(admin_unit_id, name=None):
query = EventOrganizer.query.filter(EventOrganizer.admin_unit_id == admin_unit_id)
def get_organizer_query(params: OrganizerSearchParams):
query = EventOrganizer.query.filter(
EventOrganizer.admin_unit_id == params.admin_unit_id
)
if name:
like_name = "%" + name + "%"
order_name = name + "%"
query = params.get_trackable_query(query, EventOrganizer)
if params.name:
like_name = "%" + params.name + "%"
order_name = params.name + "%"
query = params.get_trackable_order_by(query, EventOrganizer)
query = query.filter(EventOrganizer.name.ilike(like_name)).order_by(
EventOrganizer.name.ilike(order_name).desc(),
func.lower(EventOrganizer.name),
)
else:
query = params.get_trackable_order_by(query, EventOrganizer)
query = query.order_by(func.lower(EventOrganizer.name))
return query
@ -243,13 +255,15 @@ def get_custom_widget_query(admin_unit_id, name=None):
return query.order_by(func.lower(CustomWidget.name))
def get_place_query(admin_unit_id, name=None):
query = EventPlace.query.filter(EventPlace.admin_unit_id == admin_unit_id)
def get_place_query(params: EventPlaceSearchParams):
query = EventPlace.query.filter(EventPlace.admin_unit_id == params.admin_unit_id)
query = params.get_trackable_query(query, EventPlace)
if name:
like_name = "%" + name + "%"
if params.name:
like_name = "%" + params.name + "%"
query = query.filter(EventPlace.name.ilike(like_name))
query = params.get_trackable_order_by(query, EventPlace)
return query.order_by(func.lower(EventPlace.name))
@ -378,7 +392,7 @@ def create_ical_events_for_admin_unit(
from project.dateutils import get_today
from project.services.event import create_ical_events_for_search
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
params = EventSearchParams()
params.date_from = get_today() - relativedelta(months=1)

View File

@ -45,8 +45,8 @@ from project.models import (
UserFavoriteEvents,
sanitize_allday_instance,
)
from project.services.event_search import EventSearchParams
from project.services.reference import get_event_reference, upsert_event_reference
from project.services.search_params import EventSearchParams
from project.utils import get_pending_changes, get_place_str
from project.views.utils import truncate
@ -65,6 +65,8 @@ def upsert_event_category(category_name):
def fill_event_filter(event_filter, params: EventSearchParams):
event_filter = params.fill_trackable_filter(event_filter, Event)
if params.keyword:
tq = func.websearch_to_tsquery("german", params.keyword)
event_filter = and_(
@ -196,19 +198,25 @@ def fill_event_admin_unit_filter(event_filter, params: EventSearchParams):
admin_unit_reference = None
if params.admin_unit_id:
if params.include_admin_unit_references:
if params.include_admin_unit_references or params.admin_unit_references_only:
admin_unit_refs_subquery = EventReference.query.filter(
EventReference.admin_unit_id == params.admin_unit_id
).subquery()
admin_unit_reference = aliased(EventReference, admin_unit_refs_subquery)
event_filter = and_(
event_filter,
or_(
Event.admin_unit_id == params.admin_unit_id,
if params.admin_unit_references_only:
event_filter = and_(
event_filter,
admin_unit_reference.id.isnot(None),
),
)
)
else:
event_filter = and_(
event_filter,
or_(
Event.admin_unit_id == params.admin_unit_id,
admin_unit_reference.id.isnot(None),
),
)
else:
event_filter = and_(
event_filter, Event.admin_unit_id == params.admin_unit_id
@ -271,21 +279,9 @@ def get_event_dates_query(params: EventSearchParams):
.filter(event_filter)
)
if params.sort == "-rating":
if admin_unit_reference:
result = result.order_by(
case(
(
admin_unit_reference.rating.isnot(None),
admin_unit_reference.rating,
),
else_=Event.rating,
).desc()
)
else:
result = result.order_by(Event.rating.desc())
result = fill_event_query_order(result, admin_unit_reference, params)
result = result.order_by(EventDate.start)
return result
@ -395,17 +391,38 @@ def get_events_query(params: EventSearchParams):
isouter=True,
)
result = (
result.options(
contains_eager(Event.event_place).contains_eager(EventPlace.location),
joinedload(Event.categories),
joinedload(Event.organizer),
joinedload(Event.photo),
joinedload(Event.admin_unit),
)
.filter(event_filter)
.order_by(Event.min_start)
)
result = result.options(
contains_eager(Event.event_place).contains_eager(EventPlace.location),
joinedload(Event.categories),
joinedload(Event.organizer),
joinedload(Event.photo),
joinedload(Event.admin_unit),
).filter(event_filter)
result = fill_event_query_order(result, admin_unit_reference, params)
result = result.order_by(Event.min_start)
return result
def fill_event_query_order(result, admin_unit_reference, params: EventSearchParams):
result = params.get_trackable_order_by(result, Event)
if params.sort == "-rating":
if admin_unit_reference:
result = result.order_by(
case(
(
admin_unit_reference.rating.isnot(None),
admin_unit_reference.rating,
),
else_=Event.rating,
).desc()
)
else:
result = result.order_by(Event.rating.desc())
elif params.sort == "-reference_created_at" and admin_unit_reference:
result = result.order_by(admin_unit_reference.created_at.desc())
return result
@ -551,8 +568,8 @@ def populate_ical_event_with_event(
if model_event.created_at:
ical_event.add("dtstamp", model_event.created_at)
if model_event.updated_at:
ical_event.add("last-modified", model_event.updated_at)
if model_event.last_modified_at:
ical_event.add("last-modified", model_event.last_modified_at)
if model_event.status and model_event.status == EventStatus.cancelled:
ical_event.add("status", "CANCELLED")

View File

@ -27,7 +27,7 @@ def create_ical_events_for_organizer(
from project.dateutils import get_today
from project.services.event import create_ical_events_for_search
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
params = EventSearchParams()
params.date_from = get_today() - relativedelta(months=1)

View File

@ -10,6 +10,7 @@ from project.models import (
EventReferenceRequestReviewStatus,
)
from project.models.admin_unit import AdminUnit
from project.services.search_params import EventReferenceSearchParams
def create_event_reference_for_request(request):
@ -36,12 +37,24 @@ def upsert_event_reference(event_id: int, admin_unit_id: int):
return result
def get_reference_incoming_query(admin_unit):
return EventReference.query.filter(EventReference.admin_unit_id == admin_unit.id)
def get_reference_incoming_query(params: EventReferenceSearchParams):
result = EventReference.query
if params.admin_unit_id:
result = result.filter(EventReference.admin_unit_id == params.admin_unit_id)
result = params.get_trackable_query(result, EventReference)
result = params.get_trackable_order_by(result, EventReference)
result = result.order_by(EventReference.created_at.desc())
return result
def get_reference_outgoing_query(admin_unit):
return EventReference.query.join(Event).filter(Event.admin_unit_id == admin_unit.id)
return (
EventReference.query.join(Event)
.filter(Event.admin_unit_id == admin_unit.id)
.order_by(EventReference.created_at.desc())
)
def get_reference_requests_incoming_query(admin_unit):

View File

@ -1,5 +1,8 @@
from typing import Type
from dateutil.relativedelta import relativedelta
from flask import request
from sqlalchemy import and_
from project.dateutils import (
date_set_end_of_day,
@ -7,10 +10,100 @@ from project.dateutils import (
form_input_to_date,
get_today,
)
from project.models.trackable_mixin import TrackableMixin
class EventSearchParams(object):
class BaseSearchParams(object):
def __init__(self):
self.sort = None
def load_from_request(self, **kwargs):
self.sort = kwargs.get("sort", self.sort)
class TrackableSearchParams(BaseSearchParams):
def __init__(self):
super().__init__()
self.created_at_from = None
self.created_at_to = None
def load_from_request(self, **kwargs):
super().load_from_request(**kwargs)
self.created_at_from = kwargs.get("created_at_from", self.created_at_from)
self.created_at_to = kwargs.get("created_at_to", self.created_at_to)
def get_trackable_query(self, query, klass: Type[TrackableMixin]):
filter = self.fill_trackable_filter(1 == 1, klass)
return query.filter(filter)
def fill_trackable_filter(self, filter, klass: Type[TrackableMixin]):
if self.created_at_from:
filter = and_(filter, klass.created_at >= self.created_at_from)
if self.created_at_to:
filter = and_(filter, klass.created_at < self.created_at_to)
return filter
def get_trackable_order_by(self, query, klass: Type[TrackableMixin]):
if self.sort == "-created_at":
query = query.order_by(klass.created_at.desc())
elif self.sort == "-updated_at":
query = query.order_by(klass.updated_at.desc().nulls_last())
elif self.sort == "-last_modified_at":
query = query.order_by(klass.last_modified_at.desc())
return query
class EventReferenceSearchParams(TrackableSearchParams):
def __init__(self):
super().__init__()
self.admin_unit_id = None
class AdminUnitSearchParams(TrackableSearchParams):
def __init__(self):
super().__init__()
self.keyword = None
self.include_unverified = False
self.only_verifier = False
self.reference_request_for_admin_unit_id = None
def load_from_request(self, **kwargs):
super().load_from_request(**kwargs)
self.keyword = kwargs.get("keyword", self.keyword)
class OrganizerSearchParams(TrackableSearchParams):
def __init__(self):
super().__init__()
self.admin_unit_id = None
self.name = None
def load_from_request(self, **kwargs):
super().load_from_request(**kwargs)
self.name = kwargs.get("name", self.name)
class EventPlaceSearchParams(TrackableSearchParams):
def __init__(self):
super().__init__()
self.admin_unit_id = None
self.name = None
def load_from_request(self, **kwargs):
super().load_from_request(**kwargs)
self.name = kwargs.get("name", self.name)
class EventSearchParams(TrackableSearchParams):
def __init__(self):
super().__init__()
self._date_from = None
self._date_to = None
self._date_from_str = None
@ -18,6 +111,7 @@ class EventSearchParams(object):
self._coordinate = None
self.admin_unit_id = None
self.include_admin_unit_references = None
self.admin_unit_references_only = None
self.can_read_private_events = None
self.can_read_planned_events = None
self.keyword = None
@ -29,7 +123,6 @@ class EventSearchParams(object):
self.event_place_id = None
self.event_list_id = None
self.weekday = None
self.sort = None
self.status = None
self.public_status = None
self.favored_by_user_id = None
@ -112,7 +205,7 @@ class EventSearchParams(object):
return None
def load_bool_param(self, param: str):
return request.args[param] == "y"
return request.args[param].lower() in ("true", "t", "yes", "y", "on", "1")
def load_status_list_param(self):
stati = self.load_list_param("status")
@ -146,7 +239,9 @@ class EventSearchParams(object):
return result
def load_from_request(self):
def load_from_request(self, **kwargs):
super().load_from_request(**kwargs)
if "date_from" in request.args:
self.date_from_str = request.args["date_from"]
@ -183,9 +278,6 @@ class EventSearchParams(object):
if "postal_code" in request.args:
self.postal_code = self.load_list_param("postal_code")
if "sort" in request.args:
self.sort = request.args["sort"]
if "organization_id" in request.args:
self.admin_unit_id = request.args["organization_id"]
@ -200,3 +292,13 @@ class EventSearchParams(object):
if "exclude_recurring" in request.args:
self.exclude_recurring = self.load_bool_param("exclude_recurring")
if "include_organization_references" in request.args:
self.include_admin_unit_references = self.load_bool_param(
"include_organization_references"
)
if "organization_references_only" in request.args:
self.admin_unit_references_only = self.load_bool_param(
"organization_references_only"
)

View File

@ -24,7 +24,7 @@ def generate_sitemap(pinggoogle: bool):
today = get_today()
events = (
Event.query.join(Event.admin_unit)
.options(load_only(Event.id, Event.updated_at))
.options(load_only(Event.id, Event.last_modified_at))
.filter(Event.dates.any(EventDate.start >= today))
.filter(
and_(
@ -38,7 +38,11 @@ def generate_sitemap(pinggoogle: bool):
for event in events:
loc = url_for("event", event_id=event.id)
lastmod = event.updated_at.strftime("%Y-%m-%d") if event.updated_at else None
lastmod = (
event.last_modified_at.strftime("%Y-%m-%d")
if event.last_modified_at
else None
)
lastmod_tag = f"<lastmod>{lastmod}</lastmod>" if lastmod else ""
buf.write(f"<url><loc>{loc}</loc>{lastmod_tag}</url>")

View File

@ -366,7 +366,6 @@
{% macro render_audit(tracking_mixing, show_user=False) %}
{% set created_at = tracking_mixing.created_at | datetimeformat('short') %}
{% set updated_at = tracking_mixing.updated_at | datetimeformat('short') %}
{% if show_user %}
{{ _('Created at %(created_at)s by %(created_by)s.', created_at=created_at, created_by=tracking_mixing.created_by.email) }}
@ -374,11 +373,14 @@
{{ _('Created at %(created_at)s.', created_at=created_at) }}
{% endif %}
{% if created_at != updated_at %}
{% if show_user %}
{{ _('Last updated at %(updated_at)s by %(updated_by)s.', updated_at=updated_at, updated_by=tracking_mixing.updated_by.email) }}
{% else %}
{{ _('Last updated at %(updated_at)s.', updated_at=updated_at) }}
{% if tracking_mixing.updated_at %}
{% set updated_at = tracking_mixing.updated_at | datetimeformat('short') %}
{% if created_at != updated_at %}
{% if show_user %}
{{ _('Last updated at %(updated_at)s by %(updated_by)s.', updated_at=updated_at, updated_by=tracking_mixing.updated_by.email) }}
{% else %}
{{ _('Last updated at %(updated_at)s.', updated_at=updated_at) }}
{% endif %}
{% endif %}
{% endif %}
{% endmacro %}

View File

@ -13,7 +13,7 @@ from project.services.event import (
get_meta_data,
get_upcoming_event_dates,
)
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
from project.views.event import get_event_category_choices, get_user_rights
from project.views.utils import (
flash_errors,

View File

@ -14,7 +14,7 @@ from project.utils import make_dir
@app.route("/image/<int:id>/<hash>")
def image(id, hash=None):
image = Image.query.options(
load_only(Image.id, Image.encoding_format, Image.updated_at)
load_only(Image.id, Image.encoding_format, Image.last_modified_at)
).get_or_404(id)
# Dimensions

View File

@ -43,8 +43,8 @@ from project.services.admin_unit import (
get_member_for_admin_unit_by_user_id,
)
from project.services.event import get_events_query
from project.services.event_search import EventSearchParams
from project.services.event_suggestion import get_event_reviews_query
from project.services.search_params import EventSearchParams
from project.utils import get_place_str
from project.views.event import get_event_category_choices
from project.views.utils import (

View File

@ -4,7 +4,7 @@ from flask_security import auth_required
from project import app
from project.access import can_use_planning
from project.forms.planning import PlanningForm
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
from project.views.event import get_event_category_choices
from project.views.utils import permission_missing

View File

@ -2,7 +2,6 @@ from flask import abort, flash, redirect, render_template, url_for
from flask_babel import gettext
from flask_security import auth_required
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql import desc
from project import app, db
from project.access import (
@ -22,6 +21,7 @@ from project.services.reference import (
get_reference_incoming_query,
get_reference_outgoing_query,
)
from project.services.search_params import EventReferenceSearchParams
from project.views.utils import flash_errors, get_pagination_urls, handleSqlError
@ -112,11 +112,9 @@ def event_reference_update(id):
@auth_required()
def manage_admin_unit_references_incoming(id):
admin_unit = get_admin_unit_for_manage_or_404(id)
references = (
get_reference_incoming_query(admin_unit)
.order_by(desc(EventReference.created_at))
.paginate()
)
params = EventReferenceSearchParams()
params.admin_unit_id = admin_unit.id
references = get_reference_incoming_query(params).paginate()
return render_template(
"manage/references_incoming.html",
@ -130,11 +128,7 @@ def manage_admin_unit_references_incoming(id):
@auth_required()
def manage_admin_unit_references_outgoing(id):
admin_unit = get_admin_unit_for_manage_or_404(id)
references = (
get_reference_outgoing_query(admin_unit)
.order_by(desc(EventReference.created_at))
.paginate()
)
references = get_reference_outgoing_query(admin_unit).paginate()
return render_template(
"manage/references_outgoing.html",

View File

@ -15,6 +15,7 @@ from project.models import (
)
from project.models.admin_unit import AdminUnit
from project.services.admin_unit import get_admin_unit_query
from project.services.search_params import AdminUnitSearchParams
from project.services.verification import get_verification_requests_incoming_query
from project.views.utils import (
flash_errors,
@ -79,7 +80,10 @@ def manage_admin_unit_verification_requests_outgoing(id):
@manage_required("verification_request:create")
def manage_admin_unit_verification_requests_outgoing_create_select(id):
admin_unit = g.manage_admin_unit
admin_units = get_admin_unit_query(only_verifier=True).paginate()
params = AdminUnitSearchParams()
params.only_verifier = True
admin_units = get_admin_unit_query(params).paginate()
return render_template(
"manage/verification_requests_outgoing_create_select.html",

View File

@ -19,9 +19,9 @@ from project.services.event import (
get_event_date_with_details_or_404,
get_event_dates_query,
)
from project.services.event_search import EventSearchParams
from project.services.event_suggestion import insert_event_suggestion
from project.services.place import get_event_places
from project.services.search_params import EventSearchParams
from project.views.event import get_event_category_choices
from project.views.utils import (
flash_errors,

View File

@ -61,6 +61,15 @@ def test_search(client, seeder: Seeder, utils: UtilActions, app, db):
seeder.create_event(admin_unit_id, draft=True)
seeder.create_event_unverified()
url = utils.get_url("api_v1_event_date_search", sort="-created_at")
response = utils.get_json_ok(url)
url = utils.get_url("api_v1_event_date_search", sort="-updated_at")
response = utils.get_json_ok(url)
url = utils.get_url("api_v1_event_date_search", sort="-last_modified_at")
response = utils.get_json_ok(url)
url = utils.get_url("api_v1_event_date_search", sort="-rating")
response = utils.get_json_ok(url)
assert len(response.json["items"]) == 1
@ -84,6 +93,13 @@ def test_search(client, seeder: Seeder, utils: UtilActions, app, db):
)
response = utils.get_json_ok(url)
url = utils.get_url(
"api_v1_event_date_search",
created_at_from="2023-07-07T00:00:00",
created_at_to="2023-07-08T00:00:00",
)
response = utils.get_json_ok(url)
url = utils.get_url(
"api_v1_event_date_search", coordinate="51.9077888,10.4333312", distance=500
)
@ -139,6 +155,14 @@ def test_search(client, seeder: Seeder, utils: UtilActions, app, db):
response = utils.get_json_ok(url)
assert len(response.json["items"]) == 0
seeder.create_any_reference(admin_unit_id)
url = utils.get_url(
"api_v1_event_date_search",
organization_id=admin_unit_id,
sort="-reference_created_at",
)
response = utils.get_json_ok(url)
def test_search_public_status(client, seeder: Seeder, utils: UtilActions, app, db):
user_id, admin_unit_id = seeder.setup_api_access(user_access=False)

View File

@ -126,6 +126,21 @@ def test_event_search(client, seeder: Seeder, utils: UtilActions):
or response.json["items"][1]["public_status"] == "draft"
)
seeder.create_any_reference(admin_unit_id)
url = utils.get_url(
"api_v1_organization_event_search",
id=admin_unit_id,
include_organization_references="yes",
)
response = utils.get_json_ok(url)
url = utils.get_url(
"api_v1_organization_event_search",
id=admin_unit_id,
organization_references_only="yes",
)
response = utils.get_json_ok(url)
def test_organizers(client, seeder: Seeder, utils: UtilActions):
user_id, admin_unit_id = seeder.setup_api_access(user_access=False)
@ -420,7 +435,14 @@ def test_references_incoming(client, seeder: Seeder, utils: UtilActions):
url = utils.get_url(
"api_v1_organization_incoming_event_reference_list",
id=admin_unit_id,
name="crew",
)
utils.get_json_ok(url)
url = utils.get_url(
"api_v1_organization_incoming_event_reference_list",
id=admin_unit_id,
created_at_from="2023-07-07T00:00:00",
created_at_to="2023-07-08T00:00:00",
)
utils.get_json_ok(url)

View File

@ -199,7 +199,7 @@ def test_get_events_query(client, seeder, app):
with app.app_context():
from project.services.event import get_events_query
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
params = EventSearchParams()
params.admin_unit_id = admin_unit_id
@ -253,7 +253,7 @@ def test_get_events_fulltext(
with app.app_context():
from project.services.event import get_events_query
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
params = EventSearchParams()
params.keyword = keyword

View File

@ -1,6 +1,6 @@
def test_date_str(client, seeder, utils):
from project.dateutils import create_berlin_date
from project.services.event_search import EventSearchParams
from project.services.search_params import EventSearchParams
params = EventSearchParams()
params.date_from = create_berlin_date(2030, 12, 30, 0)