def test_human_readable_date(date, output): """Test that :func:`~app.models.Event.human_readable_date` properly formats event dates into human readable date strings. """ event = Event(start_date=date, start_time=None, end_date=date, end_time=None) assert event.human_readable_date() == output
def recurring_event(slug, index): """View a specific instance of a recurring event. **Route:** ``/events/<slug>/<index>`` **Methods:** ``GET`` :param str slug: The unique slug ID for the post. :param int index: The instance of the event to fetch. """ if Event.objects(published=True, slug=slug).count() == 0: abort(404) # Either invalid event ID or duplicate IDs. event = Event.objects(published=True, slug=slug)[0] if not event.is_recurring or not event.parent_series: return redirect(url_for('.event', slug=slug)) if len(event.parent_series.events) <= index: abort(404) event = event.parent_series.events[index] return render_template('events/event.html', event=event, now=now, upcoming_events=_upcoming_events_triple(event))
def register_delete_rules(self): """Registers rules for how Mongoengine handles the deletion of objects that are being referenced by other objects. See the documentation for :func:`mongoengine.model.register_delete_rule` for more information. All delete rules for User fields must by DENY, because User objects should never be deleted. Lists of reference fields should PULL, to remove deleted objects from the list, and all others should NULLIFY """ from eventum.models import (Event, EventSeries, User, Post, BlogPost, Image) from mongoengine import NULLIFY, PULL, DENY Event.register_delete_rule(EventSeries, 'events', PULL) Image.register_delete_rule(BlogPost, 'images', PULL) Image.register_delete_rule(User, 'image', NULLIFY) Image.register_delete_rule(BlogPost, 'featured_image', NULLIFY) Image.register_delete_rule(Event, 'image', NULLIFY) EventSeries.register_delete_rule(Event, 'parent_series', NULLIFY) User.register_delete_rule(Event, 'creator', DENY) User.register_delete_rule(Image, 'creator', DENY) User.register_delete_rule(Post, 'author', DENY) User.register_delete_rule(Post, 'posted_by', DENY)
def event(slug): """View a specific non-recurring event, or the next upcoming instance of a recurring event. **Route:** ``/events/<slug>`` **Methods:** ``GET`` :param str slug: The unique slug ID for the post. """ if Event.objects(published=True, slug=slug).count() == 0: abort(404) # Either invalid event ID or duplicate IDs. event = Event.objects(published=True, slug=slug)[0] if event.is_recurring: upcoming_event_instances = (Event.objects(published=True, start_date__gte=date.today(), slug=slug) .order_by('start_date')) if upcoming_event_instances: event = upcoming_event_instances[0] else: event = event.parent_series.events[-1] return render_template('events/event.html', event=event, now=now, upcoming_events=_upcoming_events_triple(event))
def create_single_event(cls, form, creator): """Creates a non-recurring Mongoengine and Google Calendar event from form data. :param form: The WTForms form. :type form: :class:`CreateEventForm` or a subclass. :param creator: The user that is currently logged in. :type creator: :class:`~app.models.User` :raises: :class:`GoogleCalendarAPIError` and it's subclasses :returns: Response from the Google Calendar API. :rtype: dict """ # Generate the event and date data event_and_date_data = DataBuilder.event_and_date_data_from_form( form, creator=creator) event_and_date_data = cls._remove_none_fields(event_and_date_data) event = Event(**event_and_date_data) event.save() # Return the Google Calendar response return e.gcal_client().create_event(event)
def create_single_event(cls, form, creator): """Creates a non-recurring Mongoengine and Google Calendar event from form data. :param form: The WTForms form. :type form: :class:`CreateEventForm` or a subclass. :param creator: The user that is currently logged in. :type creator: :class:`~app.models.User` :raises: :class:`GoogleCalendarAPIError` and it's subclasses :returns: Response from the Google Calendar API. :rtype: dict """ # Generate the event and date data event_and_date_data = DataBuilder.event_and_date_data_from_form( form, creator=creator ) event_and_date_data = cls._remove_none_fields(event_and_date_data) event = Event(**event_and_date_data) event.save() # Return the Google Calendar response return e.gcal_client().create_event(event)
def test_human_readable_time(start_time, end_time, output): """Test that :func:`~app.models.Event.human_readable_time` properly formats event times into human readable time strings. """ any_date = dt.date(2015, 3, 31) event = Event(start_date=any_date, start_time=start_time, end_date=any_date, end_time=end_time) assert event.human_readable_time() == output
def test_human_readable_atetime(sdate, stime, edate, etime, output): """Test that :func:`~app.models.Event.human_readable.datetime` properly formats event dates and times into human readable datetime strings. """ event = Event(start_date=sdate, start_time=stime, end_date=edate, end_time=etime) assert event.human_readable_datetime() == output
def test_event_starting_on_midnight(): """Test that events starting on midnight are properly formatted.""" event = Event(start_date=dt.date(2015, 4, 1), start_time=dt.time(0), end_date=dt.date(2015, 4, 1), end_time=dt.time(5, 30)) assert not event.is_multiday() assert event.human_readable_date() == "Wednesday, April 1" assert event.human_readable_time() == "12-5:30am"
def _get_events_for_template(past, future): """Returns the events to insert in the events template. Returns four groups of dates: - ``past_events``: A list of dictionaries, where the dictionaries contain a list of events for a week, and a label for the week. - ``this_week``: A list of events happening this week. - ``next_week``: A list of events happening next week. - ``future_events``: A list of dictionaries similar to ``post_events``, but for events happening in the future. :returns: ``past_events``, ``this_week``, ``next_week``, ``future_events`` """ today = date.today() last_sunday = datetime.combine( today - timedelta(days=(today.isoweekday() % 7)), datetime.min.time() ) next_sunday = last_sunday + timedelta(days=7) following_sunday = last_sunday + timedelta(days=14) this_week = (Event.objects(start_date__gte=last_sunday, start_date__lt=next_sunday) .order_by('start_date')) next_week = (Event.objects(start_date__gte=next_sunday, start_date__lt=following_sunday) .order_by('start_date')) past_events = [] future_events = [] for week_no in range(past): ending_sunday = last_sunday - timedelta(days=7 * week_no) starting_sunday = last_sunday - timedelta(days=7 * (week_no + 1)) week_name = _format_for_display(starting_sunday) events = Event.objects(start_date__gte=starting_sunday, start_date__lt=ending_sunday) past_events.insert(0, { 'week_name': week_name, 'events': events, }) for week_no in range(future): starting_sunday = following_sunday + timedelta(days=7 * week_no) ending_sunday = following_sunday + timedelta(days=7 * (week_no + 1)) week_name = _format_for_display(starting_sunday) events = Event.objects(start_date__gte=starting_sunday, start_date__lt=ending_sunday) future_events.append({ 'week_name': week_name, 'events': events, }) return past_events, this_week, next_week, future_events
def _make_event(cls, e_data, d_data): """Create a new :class:`Event` object and save it to Mongoengine. The event is created by unpacking non-None fields of ``e_data`` and ``d_data`` in the constructor for :class:`Event`. :param dict e_data: The event data for this event. :param dict d_data: The date data for this event. """ params = cls._remove_none_fields(dict(e_data.items() + d_data.items())) event = Event(**params) event.save() return event
def test_event_ending_on_midnight(): """Test that events ending on midnight are properly formatted.""" start_date, start_time = dt.date(2015, 3, 31), dt.time(22) end_date, end_time = dt.date(2015, 4, 1), dt.time(0) event = Event(start_date=start_date, start_time=start_time, end_date=end_date, end_time=end_time) assert not event.is_multiday() assert event.human_readable_date() == "Tuesday, March 31" assert event.human_readable_time() == "10pm-12am"
def test_human_readable_date(self): """Test that :func:`~app.models.Event.human_readable_date` properly formats event dates into human readable date strings. """ from eventum.models import Event for event_date, string in self.DATES: event = Event(start_date=event_date, start_time=None, end_date=event_date, end_time=None, **self.EVENT_KWARGS) msg = self.ERROR_MSG.format('human readable date', string, event.human_readable_date()) self.assertEqual(event.human_readable_date(), string, msg=msg)
def test_event_ending_on_midnight(self): """Test that events ending on midnight are properly formatted.""" from eventum.models import Event start_date, start_time = date(2015, 03, 31), time(22) end_date, end_time = date(2015, 04, 01), time(0) event = Event(start_date=start_date, start_time=start_time, end_date=end_date, end_time=end_time, **self.EVENT_KWARGS) self.assertFalse(event.is_multiday()) self.assertEqual(event.human_readable_date(), 'Tuesday, March 31') self.assertEqual(event.human_readable_time(), '10pm-12am')
def test_event_starting_on_midnight(self): """Test that events starting on midnight are properly formatted.""" from eventum.models import Event start_date, start_time = date(2015, 04, 01), time(00) end_date, end_time = date(2015, 04, 01), time(05, 30) event = Event(start_date=start_date, start_time=start_time, end_date=end_date, end_time=end_time, **self.EVENT_KWARGS) self.assertFalse(event.is_multiday()) self.assertEqual(event.human_readable_date(), 'Wednesday, April 1') self.assertEqual(event.human_readable_time(), '12-5:30am')
def index(): """View the ADI homepage. **Route:** ``/`` **Methods:** ``GET`` """ # cast date.today() to a datetime today = datetime.combine(date.today(), datetime.min.time()) # Ending on a future date, or today at a future time. The events should be # published, and should be chronological. # We limit to four events, one large event and one set of three events. events = (Event.objects(Q(end_date__gte=today)) .filter(published=True) .order_by('start_date', 'start_time') .limit(ONE_LARGE_AND_TRIPLE)) # sort published posts chronologically back in time all_blog_posts = (BlogPost.objects(published=True) .order_by('-date_published')) latest_blog_post = all_blog_posts[0] if all_blog_posts else None return render_template('index.html', events=events, blog_post=latest_blog_post)
def event_archive(index): """View old events. **Route:** ``/events/<index>`` **Methods:** ``GET`` :param int index: The page to fetch """ if index <= 0: return redirect(url_for('.events')) # Get all events that occur on this page or on subsequent pages, and order # them chronologically back in time today = date.today() events = (Event.objects(published=True, end_date__lt=today) .order_by('-start_date') .skip(NUM_PAST_EVENTS_FOR_FRONTPAGE + (index - 1) * NUM_EVENTS_PER_PAGE)) # If there are no such events, redirect to the pevious page if not events: return redirect(url_for('.event_archive', index=index - 1)) # There is always a previous page, but there is only a next page if there # are more events after this page previous_index = index - 1 next_index = index + 1 if len(events) > NUM_EVENTS_PER_PAGE else None # Use .limit() to only show NUM_EVENTS_PER_PAGE events per page return render_template('events/archive.html', events=events.limit(NUM_EVENTS_PER_PAGE), previous_index=previous_index, next_index=next_index)
def unpublish_event(self, stale_event): """Unpublish an event, moving it to the private calendar. The first argument is called ``stale_event`` because it might have outdated fields. The first thing we do is find a fresh event with it's id in mongo. :param stale_event: The event to publish :type event: :class:`Event` :raises: :class:`EventumError.GCalAPI.BadStatusLine`, :class:`EventumError.GCalAPI.NotFound`, :class:`EventumError.GCalAPI.Error`, :class:`EventumError.GCalAPI.MissingID` :returns: The Google Calendar API response. :rtype: dict """ self.before_request() # Freshen up stale_event event = Event.objects().get(id=stale_event.id) if event.published: raise EventumError.GCalAPI.PublishFailed.PublishedTrue() return self.move_event(event, from_id=self.public_calendar_id, to_id=self.private_calendar_id)
def index(): """View the ADI homepage. **Route:** ``/`` **Methods:** ``GET`` """ this_moment = datetime.now().time() # cast date.today() to a datetime today = datetime.combine(date.today(), datetime.min.time()) # Ending on a future date, or today at a future time. The events should be # published, and should be chronological. # We limit to four events, one large event and one set of three events. events = (Event.objects(Q(end_date__gte=today)) # | # Q(end_date=today, end_time__gt=this_moment)) # .filter(published=True) .order_by('start_date', 'start_time') .limit(ONE_LARGE_AND_TRIPLE)) # sort published posts chronologically back in time all_blog_posts = (BlogPost.objects(published=True) .order_by('-date_published')) latest_blog_post = all_blog_posts[0] if all_blog_posts else None return render_template('index.html', events=events, blog_post=latest_blog_post)
def test_human_readable_time(self): """Test that :func:`~app.models.Event.human_readable_time` properly formats event times into human readable time strings. """ from eventum.models import Event any_date = date(2015, 03, 31) for start_time, end_time, string in self.TIMES: event = Event(start_date=any_date, start_time=start_time, end_date=any_date, end_time=end_time, **self.EVENT_KWARGS) msg = self.ERROR_MSG.format('human readable time', string, event.human_readable_time()) self.assertEqual(event.human_readable_time(), string, msg=msg)
def delete(event_id): """Delete an existing event. **Route:** ``/admin/events/delete/<event_id>`` **Methods:** ``POST`` :param str event_id: The ID of the event to delete. """ object_id = ObjectId(event_id) form = DeleteEventForm(request.form) if Event.objects(id=object_id).count() == 1: event = Event.objects().with_id(object_id) try: EventsHelper.delete_event(event, form) except EventumError.GCalAPI as e: flash(e.message, ERROR_FLASH) else: flash('Invalid event id', ERROR_FLASH) return redirect(url_for('.index'))
def _upcoming_events_triple(event): """Returns a set of three upcoming events, excluding ``event``. :param event: The event to exclude :type event: :class:`~app.models.Event` :returns: The set of three events :rtype: Mongoengine.queryset """ return (Event.objects( published=True, start_date__gte=date.today(), id__ne=event.id).order_by('start_date').limit(ONE_TRIPLE))
def set_published_status(event_id, status): """""" object_id = ObjectId(event_id) if Event.objects(id=object_id).count() == 1: event = Event.objects().with_id(object_id) if status != event.published: event.published = status # TODO Actually publish/unpublish the event here if event.published: event.date_published = datetime.now() flash('Event published', MESSAGE_FLASH) else: event.date_published = None flash('Event unpublished', MESSAGE_FLASH) event.save() else: flash("The event had not been published. No changes made.", MESSAGE_FLASH) else: flash('Invalid event id', ERROR_FLASH) return redirect(url_for('.index'))
def _upcoming_events_triple(event): """Returns a set of three upcoming events, excluding ``event``. :param event: The event to exclude :type event: :class:`~app.models.Event` :returns: The set of three events :rtype: Mongoengine.queryset """ return (Event.objects(published=True, start_date__gte=date.today(), id__ne=event.id) .order_by('start_date') .limit(ONE_TRIPLE))
def run(self): """Run the generation. Uses the configurations passed to func:`__init__`. """ # Setup: db connection, superuser, and printer. connect(config['MONGODB_SETTINGS']['DB']) try: superuser = User.objects().get(gplus_id='super') except DoesNotExist: print ('Failed to get superuser. Try running:\n' '\texport GOOGLE_AUTH_ENABLED=TRUE') printer = ProgressPrinter(self.quiet) # Images if self.should_gen_images: if self.wipe: self.warn('Image') print CLIColor.warning('Wiping Image database.') Image.drop_collection() create_images(12, superuser, printer) # Blog posts if self.should_gen_posts: if self.wipe: self.warn('BlogPost') print CLIColor.warning('Wiping BlogPost database.') BlogPost.drop_collection() create_posts(10, superuser, printer) # Events and event series if self.should_gen_events: if self.wipe: self.warn('Event and EventSeries') print CLIColor.warning('Wiping Event database.') Event.drop_collection() print CLIColor.warning('Wiping EventSeries database.') EventSeries.drop_collection() create_events(superuser, printer)
def events(): """View the latest events. **Route:** ``/events`` **Methods:** ``GET`` """ today = date.today() weekday = (today.isoweekday() % 7) + 1 # Sun: 1, Mon: 2, ... , Sat: 7 last_sunday = datetime.combine(today - timedelta(days=weekday + 7), datetime.min.time()) next_sunday = datetime.combine(today + timedelta(days=7 - weekday), datetime.min.time()) recent_and_upcoming = Event.objects(published=True).order_by('start_date', 'start_time') # Sort recent events chronologically backwards in time recent_events = (recent_and_upcoming.filter(end_date__lt=today) .order_by('-start_date') .limit(NUM_PAST_EVENTS_FOR_FRONTPAGE)) events_this_week = list( recent_and_upcoming.filter(end_date__gte=today, start_date__lt=next_sunday) ) # One large event, and one set of three small events upcoming_events = (recent_and_upcoming.filter(start_date__gt=next_sunday) .limit(ONE_LARGE_AND_TRIPLE)) more_past_events = bool(Event.objects(published=True, start_date__lte=last_sunday).count()) return render_template('events/events.html', recent_events=recent_events, events_this_week=events_this_week, upcoming_events=upcoming_events, more_past_events=more_past_events)
def __call__(self, form, field): """Called internally by :mod:`wtforms` on validation of the field. :param form: The parent form :type form: :class:`Form` :param field: The field to validate :type field: :class:`Field` :raises: :class:`wtforms.validators.ValidationError` """ from eventum.models import Event, EventSeries if EventSeries.objects(slug=field.data).count(): raise ValidationError(self.message) if Event.objects(slug=field.data).count(): raise ValidationError(self.message)
def __call__(self, form, field): """Called internally by :mod:`wtforms` on validation of the field. :param form: The parent form :type form: :class:`Form` :param field: The field to validate :type field: :class:`Field` :raises: :class:`wtforms.validators.ValidationError` """ from eventum.models import Event, EventSeries # If we change the slug, make sure the new slug doesn't exist if self.original.slug != field.data: if EventSeries.objects(slug=field.data).count(): raise ValidationError(self.message) if Event.objects(slug=field.data).count(): raise ValidationError(self.message)
def events_this_week(): """ Get a json object containing information about all the events for the current week (Sunday to Sunday). **Route:** ``/admin/api/events/this_week **Methods:** ``GET`` """ today = date.today() last_sunday = datetime.combine( today - timedelta(days=(today.isoweekday() % 7)), datetime.min.time()) next_tuesday = last_sunday + timedelta(days=9) events = Event.objects(start_date__gte=last_sunday, start_date__lt=next_tuesday).order_by('start_date') event_dicts = [event.to_jsonifiable() for event in events] return json_success(event_dicts)
def events_this_week(): """ Get a json object containing information about all the events for the current week (Sunday to Sunday). **Route:** ``/admin/api/events/this_week **Methods:** ``GET`` """ today = date.today() last_sunday = datetime.combine( today - timedelta(days=(today.isoweekday() % 7)), datetime.min.time()) next_sunday = last_sunday + timedelta(days=7) events = Event.objects(start_date__gte=last_sunday, start_date__lt=next_sunday).order_by('start_date') event_dicts = [event.to_jsonifiable() for event in events] return json_success(event_dicts)
def index(): """The homepage of Eventum. Shows the latest blog posts and events. **Route:** ``/admin/home`` **Methods:** ``GET`` """ today = date.today() last_sunday = datetime.combine( today - timedelta(days=(today.isoweekday() % 7)), datetime.min.time()) next_sunday = last_sunday + timedelta(days=7) this_week = (Event.objects(start_date__gt=last_sunday, start_date__lt=next_sunday) .order_by('start_date')) posts = BlogPost.objects().order_by('published', '-date_published')[:5] return render_template('eventum_home.html', this_week=this_week, recent_posts=posts)
def edit(event_id): """Edit an existing event. **Route:** ``/admin/events/edit/<event_id>`` **Methods:** ``GET, POST`` :param str event_id: The ID of the event to edit. """ try: event = Event.objects().get(id=event_id) except (DoesNotExist, ValidationError): flash('Cannot find event with id "{}"'.format(event_id), ERROR_FLASH) return redirect(url_for('.index')) if request.method == "POST": form = EditEventForm(event, request.form) else: form = EventsHelper.create_form(event, request) if form.validate_on_submit(): try: EventsHelper.update_event(event, form) except EventumError.GCalAPI as e: flash(e.message, ERROR_FLASH) return redirect(url_for('.index')) if form.errors: for error in form.errors: for message in form.errors[error]: flash(message, ERROR_FLASH) delete_form = DeleteEventForm() upload_form = UploadImageForm() images = Image.objects() return render_template('eventum_events/edit.html', form=form, event=event, delete_form=delete_form, upload_form=upload_form, images=images)
def update_event(self, stale_event, as_exception=False): """Updates the event in Google Calendar. The first argument is called ``stale_event`` because it might have outdated fields. The first thing we do is find a fresh event with it's id in mongo. This method will fall back to creating a new event if we don't have reference to a ``gcal_id`` for the event, or if the update otherwise fails. :param stale_event: The event to update. :type stale_event: :class:`Event` :param bool as_exception: Whether or not this update should happen as an exception in a series. Otherwise, series' will be updated in their entirety. :raises: :class:`EventumError.GCalAPI.BadStatusLine`, :class:`EventumError.GCalAPI.NotFound`, :class:`EventumError.GCalAPI.MissingID` :returns: The Google Calendar API response. :rtype: dict """ self.before_request() # Freshen up stale_event event = Event.objects().get(id=stale_event.id) if not event.gcal_id: # If we don't have a reference if it's associate Google Calendar # ID, then create it fresh. This raises still because it # *shouldn't* ever happen, but it does. self.create_event(stale_event) raise EventumError.GCalAPI.MissingID.UpdateFellBackToCreate() resource = None resource = GoogleCalendarResourceBuilder.event_resource( event, for_update=True) calendar_id = self._calendar_id_for_event(event) # If this update should be an exception to a series of events, then # we only want to update the instance id. Otherwise, using the # ``event.gcal_id`` will update the entire series. event_id_for_update = event.gcal_id if as_exception: instance = self._instance_resource_for_event_in_series(event) instance.update(resource) resource = instance event_id_for_update = instance['id'] current_app.logger.info('[GOOGLE_CALENDAR]: Update Event') request = self.service.events().update(calendarId=calendar_id, eventId=event_id_for_update, body=resource) # Send the request, falling back to update if it fails. try: updated_event = self._execute_request(request) except EventumError.GCalAPI.NotFound as e: self.create_event(event) raise EventumError.GCalAPI.NotFound.UpdateFellBackToCreate(e=e) # Update the Event with the latest info from the response. self._update_event_from_response(event, updated_event) # Return the Google Calendar response dict return updated_event