Compare commits

..

1 Commits

Author SHA1 Message Date
Fabio Manganiello b8215d2736 A more robust cron start logic
If may happen (usually because of a race condition) that a cronjob has
already been started, but it hasn't yet changed its status from IDLE to
RUNNING when the scheduler checks it.

This fix guards the application against such events. If they occur, we
should just report them and move on, not terminate the whole scheduler.
2022-10-27 10:45:59 +02:00
542 changed files with 2277 additions and 8137 deletions

View File

@ -6,12 +6,11 @@ repos:
hooks: hooks:
# - id: trailing-whitespace # - id: trailing-whitespace
# - id: end-of-file-fixer # - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-json - id: check-json
- id: check-xml - id: check-xml
- id: check-symlinks - id: check-symlinks
- id: check-added-large-files - id: check-added-large-files
args: ['--maxkb=1500']
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2 rev: v1.1.2

View File

@ -20,7 +20,6 @@ Events
platypush/events/custom.rst platypush/events/custom.rst
platypush/events/dbus.rst platypush/events/dbus.rst
platypush/events/distance.rst platypush/events/distance.rst
platypush/events/entities.rst
platypush/events/file.rst platypush/events/file.rst
platypush/events/foursquare.rst platypush/events/foursquare.rst
platypush/events/geo.rst platypush/events/geo.rst

View File

@ -1,5 +0,0 @@
``entities``
============
.. automodule:: platypush.message.event.entities
:members:

View File

@ -1,5 +0,0 @@
``entities``
============
.. automodule:: platypush.plugins.entities
:members:

View File

@ -32,7 +32,6 @@ Plugins
platypush/plugins/db.rst platypush/plugins/db.rst
platypush/plugins/dbus.rst platypush/plugins/dbus.rst
platypush/plugins/dropbox.rst platypush/plugins/dropbox.rst
platypush/plugins/entities.rst
platypush/plugins/esp.rst platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst platypush/plugins/file.rst

View File

@ -9,13 +9,11 @@ import argparse
import logging import logging
import os import os
import sys import sys
from typing import Optional
from .bus.redis import RedisBus from .bus.redis import RedisBus
from .config import Config from .config import Config
from .context import register_backends, register_plugins from .context import register_backends, register_plugins
from .cron.scheduler import CronScheduler from .cron.scheduler import CronScheduler
from .entities import init_entities_engine, EntitiesEngine
from .event.processor import EventProcessor from .event.processor import EventProcessor
from .logger import Logger from .logger import Logger
from .message.event import Event from .message.event import Event
@ -98,7 +96,6 @@ class Daemon:
self.no_capture_stdout = no_capture_stdout self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor() self.event_processor = EventProcessor()
self.entities_engine: Optional[EntitiesEngine] = None
self.requests_to_process = requests_to_process self.requests_to_process = requests_to_process
self.processed_requests = 0 self.processed_requests = 0
self.cron_scheduler = None self.cron_scheduler = None
@ -202,25 +199,16 @@ class Daemon:
"""Stops the backends and the bus""" """Stops the backends and the bus"""
from .plugins import RunnablePlugin from .plugins import RunnablePlugin
if self.backends: for backend in self.backends.values():
for backend in self.backends.values(): backend.stop()
backend.stop()
for plugin in get_enabled_plugins().values(): for plugin in get_enabled_plugins().values():
if isinstance(plugin, RunnablePlugin): if isinstance(plugin, RunnablePlugin):
plugin.stop() plugin.stop()
if self.bus: self.bus.stop()
self.bus.stop()
self.bus = None
if self.cron_scheduler: if self.cron_scheduler:
self.cron_scheduler.stop() self.cron_scheduler.stop()
self.cron_scheduler = None
if self.entities_engine:
self.entities_engine.stop()
self.entities_engine = None
def run(self): def run(self):
"""Start the daemon""" """Start the daemon"""
@ -242,9 +230,6 @@ class Daemon:
# Initialize the plugins # Initialize the plugins
register_plugins(bus=self.bus) register_plugins(bus=self.bus)
# Initialize the entities engine
self.entities_engine = init_entities_engine()
# Start the cron scheduler # Start the cron scheduler
if Config.get_cronjobs(): if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs()) self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())

View File

@ -3,7 +3,8 @@ import os
from typing import Optional, Union, List, Dict, Any from typing import Optional, Union, List, Dict, Any
from sqlalchemy import create_engine, Column, Integer, String, DateTime from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from platypush.backend import Backend from platypush.backend import Backend
from platypush.config import Config from platypush.config import Config
@ -16,10 +17,10 @@ Session = scoped_session(sessionmaker())
class Covid19Update(Base): class Covid19Update(Base):
"""Models the Covid19Data table""" """ Models the Covid19Data table """
__tablename__ = 'covid19data' __tablename__ = 'covid19data'
__table_args__ = {'sqlite_autoincrement': True} __table_args__ = ({'sqlite_autoincrement': True})
country = Column(String, primary_key=True) country = Column(String, primary_key=True)
confirmed = Column(Integer, nullable=False, default=0) confirmed = Column(Integer, nullable=False, default=0)
@ -39,12 +40,7 @@ class Covid19Backend(Backend):
""" """
# noinspection PyProtectedMember # noinspection PyProtectedMember
def __init__( def __init__(self, country: Optional[Union[str, List[str]]], poll_seconds: Optional[float] = 3600.0, **kwargs):
self,
country: Optional[Union[str, List[str]]],
poll_seconds: Optional[float] = 3600.0,
**kwargs
):
""" """
:param country: Default country (or list of countries) to retrieve the stats for. It can either be the full :param country: Default country (or list of countries) to retrieve the stats for. It can either be the full
country name or the country code. Special values: country name or the country code. Special values:
@ -60,9 +56,7 @@ class Covid19Backend(Backend):
super().__init__(poll_seconds=poll_seconds, **kwargs) super().__init__(poll_seconds=poll_seconds, **kwargs)
self._plugin: Covid19Plugin = get_plugin('covid19') self._plugin: Covid19Plugin = get_plugin('covid19')
self.country: List[str] = self._plugin._get_countries(country) self.country: List[str] = self._plugin._get_countries(country)
self.workdir = os.path.join( self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'covid19')
os.path.expanduser(Config.get('workdir')), 'covid19'
)
self.dbfile = os.path.join(self.workdir, 'data.db') self.dbfile = os.path.join(self.workdir, 'data.db')
os.makedirs(self.workdir, exist_ok=True) os.makedirs(self.workdir, exist_ok=True)
@ -73,30 +67,22 @@ class Covid19Backend(Backend):
self.logger.info('Stopped Covid19 backend') self.logger.info('Stopped Covid19 backend')
def _process_update(self, summary: Dict[str, Any], session: Session): def _process_update(self, summary: Dict[str, Any], session: Session):
update_time = datetime.datetime.fromisoformat( update_time = datetime.datetime.fromisoformat(summary['Date'].replace('Z', '+00:00'))
summary['Date'].replace('Z', '+00:00')
)
self.bus.post( self.bus.post(Covid19UpdateEvent(
Covid19UpdateEvent( country=summary['Country'],
country=summary['Country'], country_code=summary['CountryCode'],
country_code=summary['CountryCode'], confirmed=summary['TotalConfirmed'],
confirmed=summary['TotalConfirmed'], deaths=summary['TotalDeaths'],
deaths=summary['TotalDeaths'], recovered=summary['TotalRecovered'],
recovered=summary['TotalRecovered'], update_time=update_time,
update_time=update_time, ))
)
)
session.merge( session.merge(Covid19Update(country=summary['CountryCode'],
Covid19Update( confirmed=summary['TotalConfirmed'],
country=summary['CountryCode'], deaths=summary['TotalDeaths'],
confirmed=summary['TotalConfirmed'], recovered=summary['TotalRecovered'],
deaths=summary['TotalDeaths'], last_updated_at=update_time))
recovered=summary['TotalRecovered'],
last_updated_at=update_time,
)
)
def loop(self): def loop(self):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -104,30 +90,23 @@ class Covid19Backend(Backend):
if not summaries: if not summaries:
return return
engine = create_engine( engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
session = Session() session = Session()
last_records = { last_records = {
record.country: record record.country: record
for record in session.query(Covid19Update) for record in session.query(Covid19Update).filter(Covid19Update.country.in_(self.country)).all()
.filter(Covid19Update.country.in_(self.country))
.all()
} }
for summary in summaries: for summary in summaries:
country = summary['CountryCode'] country = summary['CountryCode']
last_record = last_records.get(country) last_record = last_records.get(country)
if ( if not last_record or \
not last_record summary['TotalConfirmed'] != last_record.confirmed or \
or summary['TotalConfirmed'] != last_record.confirmed summary['TotalDeaths'] != last_record.deaths or \
or summary['TotalDeaths'] != last_record.deaths summary['TotalRecovered'] != last_record.recovered:
or summary['TotalRecovered'] != last_record.recovered
):
self._process_update(summary=summary, session=session) self._process_update(summary=summary, session=session)
session.commit() session.commit()

View File

@ -6,28 +6,15 @@ from typing import Optional, List
import requests import requests
from sqlalchemy import create_engine, Column, String, DateTime from sqlalchemy import create_engine, Column, String, DateTime
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from platypush.backend import Backend from platypush.backend import Backend
from platypush.config import Config from platypush.config import Config
from platypush.message.event.github import ( from platypush.message.event.github import GithubPushEvent, GithubCommitCommentEvent, GithubCreateEvent, \
GithubPushEvent, GithubDeleteEvent, GithubEvent, GithubForkEvent, GithubWikiEvent, GithubIssueCommentEvent, GithubIssueEvent, \
GithubCommitCommentEvent, GithubMemberEvent, GithubPublicEvent, GithubPullRequestEvent, GithubPullRequestReviewCommentEvent, \
GithubCreateEvent, GithubReleaseEvent, GithubSponsorshipEvent, GithubWatchEvent
GithubDeleteEvent,
GithubEvent,
GithubForkEvent,
GithubWikiEvent,
GithubIssueCommentEvent,
GithubIssueEvent,
GithubMemberEvent,
GithubPublicEvent,
GithubPullRequestEvent,
GithubPullRequestReviewCommentEvent,
GithubReleaseEvent,
GithubSponsorshipEvent,
GithubWatchEvent,
)
Base = declarative_base() Base = declarative_base()
Session = scoped_session(sessionmaker()) Session = scoped_session(sessionmaker())
@ -84,17 +71,8 @@ class GithubBackend(Backend):
_base_url = 'https://api.github.com' _base_url = 'https://api.github.com'
def __init__( def __init__(self, user: str, user_token: str, repos: Optional[List[str]] = None, org: Optional[str] = None,
self, poll_seconds: int = 60, max_events_per_scan: Optional[int] = 10, *args, **kwargs):
user: str,
user_token: str,
repos: Optional[List[str]] = None,
org: Optional[str] = None,
poll_seconds: int = 60,
max_events_per_scan: Optional[int] = 10,
*args,
**kwargs
):
""" """
If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level. If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level.
@ -124,23 +102,17 @@ class GithubBackend(Backend):
def _request(self, uri: str, method: str = 'get') -> dict: def _request(self, uri: str, method: str = 'get') -> dict:
method = getattr(requests, method.lower()) method = getattr(requests, method.lower())
return method( return method(self._base_url + uri, auth=(self.user, self.user_token),
self._base_url + uri, headers={'Accept': 'application/vnd.github.v3+json'}).json()
auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'},
).json()
def _init_db(self): def _init_db(self):
engine = create_engine( engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
@staticmethod @staticmethod
def _to_datetime(time_string: str) -> datetime.datetime: def _to_datetime(time_string: str) -> datetime.datetime:
"""Convert ISO 8061 string format with leading 'Z' into something understandable by Python""" """ Convert ISO 8061 string format with leading 'Z' into something understandable by Python """
return datetime.datetime.fromisoformat(time_string[:-1] + '+00:00') return datetime.datetime.fromisoformat(time_string[:-1] + '+00:00')
@staticmethod @staticmethod
@ -156,11 +128,7 @@ class GithubBackend(Backend):
def _get_last_event_time(self, uri: str): def _get_last_event_time(self, uri: str):
with self.db_lock: with self.db_lock:
record = self._get_or_create_resource(uri=uri, session=Session()) record = self._get_or_create_resource(uri=uri, session=Session())
return ( return record.last_updated_at.replace(tzinfo=datetime.timezone.utc) if record.last_updated_at else None
record.last_updated_at.replace(tzinfo=datetime.timezone.utc)
if record.last_updated_at
else None
)
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime): def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
with self.db_lock: with self.db_lock:
@ -190,18 +158,9 @@ class GithubBackend(Backend):
'WatchEvent': GithubWatchEvent, 'WatchEvent': GithubWatchEvent,
} }
event_type = ( event_type = event_mapping[event['type']] if event['type'] in event_mapping else GithubEvent
event_mapping[event['type']] return event_type(event_type=event['type'], actor=event['actor'], repo=event.get('repo', {}),
if event['type'] in event_mapping payload=event['payload'], created_at=cls._to_datetime(event['created_at']))
else GithubEvent
)
return event_type(
event_type=event['type'],
actor=event['actor'],
repo=event.get('repo', {}),
payload=event['payload'],
created_at=cls._to_datetime(event['created_at']),
)
def _events_monitor(self, uri: str, method: str = 'get'): def _events_monitor(self, uri: str, method: str = 'get'):
def thread(): def thread():
@ -216,10 +175,7 @@ class GithubBackend(Backend):
fired_events = [] fired_events = []
for event in events: for event in events:
if ( if self.max_events_per_scan and len(fired_events) >= self.max_events_per_scan:
self.max_events_per_scan
and len(fired_events) >= self.max_events_per_scan
):
break break
event_time = self._to_datetime(event['created_at']) event_time = self._to_datetime(event['created_at'])
@ -233,19 +189,14 @@ class GithubBackend(Backend):
for event in fired_events: for event in fired_events:
self.bus.post(event) self.bus.post(event)
self._update_last_event_time( self._update_last_event_time(uri=uri, last_updated_at=new_last_event_time)
uri=uri, last_updated_at=new_last_event_time
)
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning('Encountered exception while fetching events from {}: {}'.format(
'Encountered exception while fetching events from {}: {}'.format( uri, str(e)))
uri, str(e)
)
)
self.logger.exception(e) self.logger.exception(e)
finally:
if self.wait_stop(timeout=self.poll_seconds): if self.wait_stop(timeout=self.poll_seconds):
break break
return thread return thread
@ -255,30 +206,12 @@ class GithubBackend(Backend):
if self.repos: if self.repos:
for repo in self.repos: for repo in self.repos:
monitors.append( monitors.append(threading.Thread(target=self._events_monitor('/networks/{repo}/events'.format(repo=repo))))
threading.Thread(
target=self._events_monitor(
'/networks/{repo}/events'.format(repo=repo)
)
)
)
if self.org: if self.org:
monitors.append( monitors.append(threading.Thread(target=self._events_monitor('/orgs/{org}/events'.format(org=self.org))))
threading.Thread(
target=self._events_monitor(
'/orgs/{org}/events'.format(org=self.org)
)
)
)
if not (self.repos or self.org): if not (self.repos or self.org):
monitors.append( monitors.append(threading.Thread(target=self._events_monitor('/users/{user}/events'.format(user=self.user))))
threading.Thread(
target=self._events_monitor(
'/users/{user}/events'.format(user=self.user)
)
)
)
for monitor in monitors: for monitor in monitors:
monitor.start() monitor.start()
@ -289,5 +222,4 @@ class GithubBackend(Backend):
self.logger.info('Github backend terminated') self.logger.info('Github backend terminated')
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,7 +1,6 @@
import json import json
from flask import Blueprint, abort, request from flask import Blueprint, abort, request, Response
from flask.wrappers import Response
from platypush.backend.http.app import template_folder from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, logger, send_message from platypush.backend.http.app.utils import authenticate, logger, send_message
@ -15,8 +14,8 @@ __routes__ = [
@execute.route('/execute', methods=['POST']) @execute.route('/execute', methods=['POST'])
@authenticate(json=True) @authenticate()
def execute_route(): def execute():
"""Endpoint to execute commands""" """Endpoint to execute commands"""
try: try:
msg = json.loads(request.data.decode('utf-8')) msg = json.loads(request.data.decode('utf-8'))

View File

@ -8,27 +8,22 @@ from platypush.backend.http.app import template_folder
img_folder = os.path.join(template_folder, 'img') img_folder = os.path.join(template_folder, 'img')
fonts_folder = os.path.join(template_folder, 'fonts')
icons_folder = os.path.join(template_folder, 'icons') icons_folder = os.path.join(template_folder, 'icons')
resources = Blueprint('resources', __name__, template_folder=template_folder) resources = Blueprint('resources', __name__, template_folder=template_folder)
favicon = Blueprint('favicon', __name__, template_folder=template_folder) favicon = Blueprint('favicon', __name__, template_folder=template_folder)
img = Blueprint('img', __name__, template_folder=template_folder) img = Blueprint('img', __name__, template_folder=template_folder)
icons = Blueprint('icons', __name__, template_folder=template_folder)
fonts = Blueprint('fonts', __name__, template_folder=template_folder)
# Declare routes list # Declare routes list
__routes__ = [ __routes__ = [
resources, resources,
favicon, favicon,
img, img,
icons,
fonts,
] ]
@resources.route('/resources/<path:path>', methods=['GET']) @resources.route('/resources/<path:path>', methods=['GET'])
def resources_path(path): def resources_path(path):
"""Custom static resources""" """ Custom static resources """
path_tokens = path.split('/') path_tokens = path.split('/')
http_conf = Config.get('backend.http') http_conf = Config.get('backend.http')
resource_dirs = http_conf.get('resource_dirs', {}) resource_dirs = http_conf.get('resource_dirs', {})
@ -47,11 +42,9 @@ def resources_path(path):
real_path = real_base_path real_path = real_base_path
file_path = [ file_path = [
s s for s in re.sub(
for s in re.sub( r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection] ).split('/') if s
).split('/')
if s
] ]
for p in file_path[:-1]: for p in file_path[:-1]:
@ -68,26 +61,20 @@ def resources_path(path):
@favicon.route('/favicon.ico', methods=['GET']) @favicon.route('/favicon.ico', methods=['GET'])
def serve_favicon(): def serve_favicon():
"""favicon.ico icon""" """ favicon.ico icon """
return send_from_directory(template_folder, 'favicon.ico') return send_from_directory(template_folder, 'favicon.ico')
@img.route('/img/<path:path>', methods=['GET']) @img.route('/img/<path:path>', methods=['GET'])
def imgpath(path): def imgpath(path):
"""Default static images""" """ Default static images """
return send_from_directory(img_folder, path) return send_from_directory(img_folder, path)
@icons.route('/icons/<path:path>', methods=['GET']) @img.route('/icons/<path:path>', methods=['GET'])
def iconpath(path): def iconpath(path):
"""Default static icons""" """ Default static icons """
return send_from_directory(icons_folder, path) return send_from_directory(icons_folder, path)
@fonts.route('/fonts/<path:path>', methods=['GET'])
def fontpath(path):
"""Default fonts"""
return send_from_directory(fonts_folder, path)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -3,8 +3,7 @@ import logging
import os import os
from functools import wraps from functools import wraps
from flask import abort, request, redirect, jsonify, current_app from flask import abort, request, redirect, Response, current_app
from flask.wrappers import Response
from redis import Redis from redis import Redis
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default # NOTE: The HTTP service will *only* work on top of a Redis bus. The default
@ -185,37 +184,7 @@ def _authenticate_csrf_token():
) )
def authenticate( def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=False):
redirect_page='',
skip_auth_methods=None,
check_csrf_token=False,
json=False,
):
def on_auth_fail(has_users=True):
if json:
if has_users:
return (
jsonify(
{
'message': 'Not logged in',
}
),
401,
)
return (
jsonify(
{
'message': 'Please register a user through '
'the web panel first',
}
),
412,
)
target_page = 'login' if has_users else 'register'
return redirect(f'/{target_page}?redirect={redirect_page or request.url}', 307)
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@ -244,7 +213,9 @@ def authenticate(
if session_auth_ok: if session_auth_ok:
return f(*args, **kwargs) return f(*args, **kwargs)
return on_auth_fail() return redirect(
'/login?redirect=' + (redirect_page or request.url), 307
)
# CSRF token check # CSRF token check
if check_csrf_token: if check_csrf_token:
@ -253,7 +224,9 @@ def authenticate(
return abort(403, 'Invalid or missing csrf_token') return abort(403, 'Invalid or missing csrf_token')
if n_users == 0 and 'session' not in skip_methods: if n_users == 0 and 'session' not in skip_methods:
return on_auth_fail(has_users=False) return redirect(
'/register?redirect=' + (redirect_page or request.url), 307
)
if ( if (
('http' not in skip_methods and http_auth_ok) ('http' not in skip_methods and http_auth_ok)

View File

@ -2,17 +2,11 @@ import datetime
import enum import enum
import os import os
from sqlalchemy import ( from sqlalchemy import create_engine, Column, Integer, String, DateTime, \
create_engine, Enum, ForeignKey
Column,
Integer,
String,
DateTime,
Enum,
ForeignKey,
)
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from platypush.backend.http.request import HttpRequest from platypush.backend.http.request import HttpRequest
@ -50,31 +44,18 @@ class RssUpdates(HttpRequest):
""" """
user_agent = ( user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' + \
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/62.0.3202.94 Safari/537.36'
+ 'Chrome/62.0.3202.94 Safari/537.36'
)
def __init__( def __init__(self, url, title=None, headers=None, params=None, max_entries=None,
self, extract_content=False, digest_format=None, user_agent: str = user_agent,
url, body_style: str = 'font-size: 22px; ' +
title=None, 'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;',
headers=None, title_style: str = 'margin-top: 30px',
params=None, subtitle_style: str = 'margin-top: 10px; page-break-after: always',
max_entries=None, article_title_style: str = 'page-break-before: always',
extract_content=False, article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted',
digest_format=None, article_content_style: str = '', *argv, **kwargs):
user_agent: str = user_agent,
body_style: str = 'font-size: 22px; '
+ 'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;',
title_style: str = 'margin-top: 30px',
subtitle_style: str = 'margin-top: 10px; page-break-after: always',
article_title_style: str = 'page-break-before: always',
article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted',
article_content_style: str = '',
*argv,
**kwargs,
):
""" """
:param url: URL to the RSS feed to be monitored. :param url: URL to the RSS feed to be monitored.
:param title: Optional title for the feed. :param title: Optional title for the feed.
@ -110,9 +91,7 @@ class RssUpdates(HttpRequest):
# If true, then the http.webpage plugin will be used to parse the content # If true, then the http.webpage plugin will be used to parse the content
self.extract_content = extract_content self.extract_content = extract_content
self.digest_format = ( self.digest_format = digest_format.lower() if digest_format else None # Supported formats: html, pdf
digest_format.lower() if digest_format else None
) # Supported formats: html, pdf
os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True) os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True)
@ -140,11 +119,7 @@ class RssUpdates(HttpRequest):
@staticmethod @staticmethod
def _get_latest_update(session, source_id): def _get_latest_update(session, source_id):
return ( return session.query(func.max(FeedEntry.published)).filter_by(source_id=source_id).scalar()
session.query(func.max(FeedEntry.published))
.filter_by(source_id=source_id)
.scalar()
)
def _parse_entry_content(self, link): def _parse_entry_content(self, link):
self.logger.info('Extracting content from {}'.format(link)) self.logger.info('Extracting content from {}'.format(link))
@ -155,20 +130,14 @@ class RssUpdates(HttpRequest):
errors = response.errors errors = response.errors
if not output: if not output:
self.logger.warning( self.logger.warning('Mercury parser error: {}'.format(errors or '[unknown error]'))
'Mercury parser error: {}'.format(errors or '[unknown error]')
)
return return
return output.get('content') return output.get('content')
def get_new_items(self, response): def get_new_items(self, response):
import feedparser import feedparser
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
Session.configure(bind=engine) Session.configure(bind=engine)
@ -188,16 +157,12 @@ class RssUpdates(HttpRequest):
content = u''' content = u'''
<h1 style="{title_style}">{title}</h1> <h1 style="{title_style}">{title}</h1>
<h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.format( <h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.\
title_style=self.title_style, format(title_style=self.title_style, title=self.title, subtitle_style=self.subtitle_style,
title=self.title, creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M'))
subtitle_style=self.subtitle_style,
creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M'),
)
self.logger.info( self.logger.info('Parsed {:d} items from RSS feed <{}>'
'Parsed {:d} items from RSS feed <{}>'.format(len(feed.entries), self.url) .format(len(feed.entries), self.url))
)
for entry in feed.entries: for entry in feed.entries:
if not entry.published_parsed: if not entry.published_parsed:
@ -206,10 +171,9 @@ class RssUpdates(HttpRequest):
try: try:
entry_timestamp = datetime.datetime(*entry.published_parsed[:6]) entry_timestamp = datetime.datetime(*entry.published_parsed[:6])
if latest_update is None or entry_timestamp > latest_update: if latest_update is None \
self.logger.info( or entry_timestamp > latest_update:
'Processed new item from RSS feed <{}>'.format(self.url) self.logger.info('Processed new item from RSS feed <{}>'.format(self.url))
)
entry.summary = entry.summary if hasattr(entry, 'summary') else None entry.summary = entry.summary if hasattr(entry, 'summary') else None
if self.extract_content: if self.extract_content:
@ -224,13 +188,9 @@ class RssUpdates(HttpRequest):
<a href="{link}" target="_blank" style="{article_link_style}">{title}</a> <a href="{link}" target="_blank" style="{article_link_style}">{title}</a>
</h1> </h1>
<div class="_parsed-content" style="{article_content_style}">{content}</div>'''.format( <div class="_parsed-content" style="{article_content_style}">{content}</div>'''.format(
article_title_style=self.article_title_style, article_title_style=self.article_title_style, article_link_style=self.article_link_style,
article_link_style=self.article_link_style, article_content_style=self.article_content_style, link=entry.link, title=entry.title,
article_content_style=self.article_content_style, content=entry.content)
link=entry.link,
title=entry.title,
content=entry.content,
)
e = { e = {
'entry_id': entry.id, 'entry_id': entry.id,
@ -247,32 +207,21 @@ class RssUpdates(HttpRequest):
if self.max_entries and len(entries) > self.max_entries: if self.max_entries and len(entries) > self.max_entries:
break break
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning('Exception encountered while parsing RSS ' +
'Exception encountered while parsing RSS ' 'RSS feed {}: {}'.format(entry.link, str(e)))
+ f'RSS feed {entry.link}: {e}'
)
self.logger.exception(e) self.logger.exception(e)
source_record.last_updated_at = parse_start_time source_record.last_updated_at = parse_start_time
digest_filename = None digest_filename = None
if entries: if entries:
self.logger.info( self.logger.info('Parsed {} new entries from the RSS feed {}'.format(
'Parsed {} new entries from the RSS feed {}'.format( len(entries), self.title))
len(entries), self.title
)
)
if self.digest_format: if self.digest_format:
digest_filename = os.path.join( digest_filename = os.path.join(self.workdir, 'cache', '{}_{}.{}'.format(
self.workdir, datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
'cache', self.title, self.digest_format))
'{}_{}.{}'.format(
datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
self.title,
self.digest_format,
),
)
os.makedirs(os.path.dirname(digest_filename), exist_ok=True) os.makedirs(os.path.dirname(digest_filename), exist_ok=True)
@ -284,15 +233,12 @@ class RssUpdates(HttpRequest):
</head> </head>
<body style="{body_style}">{content}</body> <body style="{body_style}">{content}</body>
</html> </html>
'''.format( '''.format(title=self.title, body_style=self.body_style, content=content)
title=self.title, body_style=self.body_style, content=content
)
with open(digest_filename, 'w', encoding='utf-8') as f: with open(digest_filename, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
elif self.digest_format == 'pdf': elif self.digest_format == 'pdf':
from weasyprint import HTML, CSS from weasyprint import HTML, CSS
try: try:
from weasyprint.fonts import FontConfiguration from weasyprint.fonts import FontConfiguration
except ImportError: except ImportError:
@ -300,47 +246,37 @@ class RssUpdates(HttpRequest):
body_style = 'body { ' + self.body_style + ' }' body_style = 'body { ' + self.body_style + ' }'
font_config = FontConfiguration() font_config = FontConfiguration()
css = [ css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
CSS('https://fonts.googleapis.com/css?family=Merriweather'), CSS(string=body_style, font_config=font_config)]
CSS(string=body_style, font_config=font_config),
]
HTML(string=content).write_pdf(digest_filename, stylesheets=css) HTML(string=content).write_pdf(digest_filename, stylesheets=css)
else: else:
raise RuntimeError( raise RuntimeError('Unsupported format: {}. Supported formats: ' +
f'Unsupported format: {self.digest_format}. Supported formats: html, pdf' 'html or pdf'.format(self.digest_format))
)
digest_entry = FeedDigest( digest_entry = FeedDigest(source_id=source_record.id,
source_id=source_record.id, format=self.digest_format,
format=self.digest_format, filename=digest_filename)
filename=digest_filename,
)
session.add(digest_entry) session.add(digest_entry)
self.logger.info( self.logger.info('{} digest ready: {}'.format(self.digest_format, digest_filename))
'{} digest ready: {}'.format(self.digest_format, digest_filename)
)
session.commit() session.commit()
self.logger.info('Parsing RSS feed {}: completed'.format(self.title)) self.logger.info('Parsing RSS feed {}: completed'.format(self.title))
return NewFeedEvent( return NewFeedEvent(request=dict(self), response=entries,
request=dict(self), source_id=source_record.id,
response=entries, source_title=source_record.title,
source_id=source_record.id, title=self.title,
source_title=source_record.title, digest_format=self.digest_format,
title=self.title, digest_filename=digest_filename)
digest_format=self.digest_format,
digest_filename=digest_filename,
)
class FeedSource(Base): class FeedSource(Base):
"""Models the FeedSource table, containing RSS sources to be parsed""" """ Models the FeedSource table, containing RSS sources to be parsed """
__tablename__ = 'FeedSource' __tablename__ = 'FeedSource'
__table_args__ = {'sqlite_autoincrement': True} __table_args__ = ({'sqlite_autoincrement': True})
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
title = Column(String) title = Column(String)
@ -349,10 +285,10 @@ class FeedSource(Base):
class FeedEntry(Base): class FeedEntry(Base):
"""Models the FeedEntry table, which contains RSS entries""" """ Models the FeedEntry table, which contains RSS entries """
__tablename__ = 'FeedEntry' __tablename__ = 'FeedEntry'
__table_args__ = {'sqlite_autoincrement': True} __table_args__ = ({'sqlite_autoincrement': True})
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
entry_id = Column(String) entry_id = Column(String)
@ -365,15 +301,15 @@ class FeedEntry(Base):
class FeedDigest(Base): class FeedDigest(Base):
"""Models the FeedDigest table, containing feed digests either in HTML """ Models the FeedDigest table, containing feed digests either in HTML
or PDF format""" or PDF format """
class DigestFormat(enum.Enum): class DigestFormat(enum.Enum):
html = 1 html = 1
pdf = 2 pdf = 2
__tablename__ = 'FeedDigest' __tablename__ = 'FeedDigest'
__table_args__ = {'sqlite_autoincrement': True} __table_args__ = ({'sqlite_autoincrement': True})
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
source_id = Column(Integer, ForeignKey('FeedSource.id'), nullable=False) source_id = Column(Integer, ForeignKey('FeedSource.id'), nullable=False)
@ -381,5 +317,4 @@ class FeedDigest(Base):
filename = Column(String, nullable=False) filename = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,7 +0,0 @@
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./Poppins.ttf) format('truetype');
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.0ecd5641.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.3b5b9cec.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.c27e0a41.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.602f8c67.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.5adf1720.js"></script><script defer="defer" type="module" src="/static/js/app.12b17001.js"></script><link href="/static/css/chunk-vendors.5cf89a0c.css" rel="stylesheet"><link href="/static/css/app.5028a669.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.6835b8d0.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.ab28664f.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"static/js/1595-legacy.69aea4ae.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,4B,eAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,SAC/B,EAEDX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,SAC/B,EAEDL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,YAC/B,GAGHI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,KAEZ,EAEDC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,IAChB,GAGHG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,IAC/B,G,UCxDH,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

View File

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(6813),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]); "use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(2628),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]);
//# sourceMappingURL=1595-legacy.69aea4ae.js.map //# sourceMappingURL=1595-legacy.ddcdc704.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/1595-legacy.ddcdc704.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,6B,cAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,WAGhCX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,WAGhCL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,eAIlCI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,OAIbC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,OAInBG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,O,UCvDlC,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

Some files were not shown because too many files have changed in this diff Show More