Update atst to atat
This commit is contained in:
49
atat/utils/__init__.py
Normal file
49
atat/utils/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from atat.database import db
|
||||
from atat.domain.exceptions import AlreadyExistsError
|
||||
|
||||
|
||||
def first_or_none(predicate, lst):
|
||||
return next((x for x in lst if predicate(x)), None)
|
||||
|
||||
|
||||
def getattr_path(obj, path, default=None):
|
||||
_obj = obj
|
||||
for item in path.split("."):
|
||||
if isinstance(_obj, dict):
|
||||
_obj = _obj.get(item)
|
||||
else:
|
||||
_obj = getattr(_obj, item, default)
|
||||
return _obj
|
||||
|
||||
|
||||
def camel_to_snake(camel_cased):
|
||||
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_cased)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||
|
||||
|
||||
def snake_to_camel(snake_cased):
|
||||
parts = snake_cased.split("_")
|
||||
return f"{parts[0]}{''.join([w.capitalize() for w in parts[1:]])}"
|
||||
|
||||
|
||||
def pick(keys, dct):
|
||||
_keys = set(keys)
|
||||
return {k: v for (k, v) in dct.items() if k in _keys}
|
||||
|
||||
|
||||
def commit_or_raise_already_exists_error(message):
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError(message)
|
||||
|
||||
|
||||
def sha256_hex(string):
|
||||
hsh = hashlib.sha256(string.encode())
|
||||
return hsh.digest().hex()
|
128
atat/utils/context_processors.py
Normal file
128
atat/utils/context_processors.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from flask import g
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from atat.database import db
|
||||
from atat.domain.authz import Authorization
|
||||
from atat.domain.exceptions import NotFoundError
|
||||
from atat.domain.portfolios.scopes import ScopedPortfolio
|
||||
from atat.models import (
|
||||
Application,
|
||||
Environment,
|
||||
Permissions,
|
||||
Portfolio,
|
||||
PortfolioInvitation,
|
||||
PortfolioRole,
|
||||
TaskOrder,
|
||||
)
|
||||
|
||||
|
||||
def get_resources_from_context(view_args):
|
||||
query = None
|
||||
|
||||
if view_args is None:
|
||||
view_args = {}
|
||||
|
||||
if "portfolio_token" in view_args:
|
||||
query = (
|
||||
db.session.query(Portfolio)
|
||||
.join(PortfolioRole, PortfolioRole.portfolio_id == Portfolio.id)
|
||||
.join(
|
||||
PortfolioInvitation,
|
||||
PortfolioInvitation.portfolio_role_id == PortfolioRole.id,
|
||||
)
|
||||
.filter(PortfolioInvitation.token == view_args["portfolio_token"])
|
||||
)
|
||||
|
||||
elif "portfolio_id" in view_args:
|
||||
query = db.session.query(Portfolio).filter(
|
||||
Portfolio.id == view_args["portfolio_id"]
|
||||
)
|
||||
|
||||
elif "application_id" in view_args:
|
||||
query = (
|
||||
db.session.query(Portfolio, Application)
|
||||
.join(Application, Application.portfolio_id == Portfolio.id)
|
||||
.filter(Application.id == view_args["application_id"])
|
||||
)
|
||||
|
||||
elif "environment_id" in view_args:
|
||||
query = (
|
||||
db.session.query(Portfolio, Application)
|
||||
.join(Application, Application.portfolio_id == Portfolio.id)
|
||||
.join(Environment, Environment.application_id == Application.id)
|
||||
.filter(Environment.id == view_args["environment_id"])
|
||||
)
|
||||
|
||||
elif "task_order_id" in view_args:
|
||||
query = (
|
||||
db.session.query(Portfolio, TaskOrder)
|
||||
.join(TaskOrder, TaskOrder.portfolio_id == Portfolio.id)
|
||||
.filter(TaskOrder.id == view_args["task_order_id"])
|
||||
)
|
||||
|
||||
if query:
|
||||
try:
|
||||
return query.only_return_tuples(True).one()
|
||||
except NoResultFound:
|
||||
raise NotFoundError("portfolio")
|
||||
|
||||
|
||||
def assign_resources(view_args):
|
||||
g.portfolio = None
|
||||
g.application = None
|
||||
g.task_order = None
|
||||
|
||||
resources = get_resources_from_context(view_args)
|
||||
if resources:
|
||||
for resource in resources:
|
||||
if isinstance(resource, Portfolio):
|
||||
g.portfolio = ScopedPortfolio(g.current_user, resource)
|
||||
elif isinstance(resource, Application):
|
||||
g.application = resource
|
||||
elif isinstance(resource, TaskOrder):
|
||||
g.task_order = resource
|
||||
|
||||
|
||||
def user_can_view(permission):
|
||||
if g.application:
|
||||
return Authorization.has_application_permission(
|
||||
g.current_user, g.application, permission
|
||||
)
|
||||
elif g.portfolio:
|
||||
return Authorization.has_portfolio_permission(
|
||||
g.current_user, g.portfolio, permission
|
||||
)
|
||||
else:
|
||||
return Authorization.has_atat_permission(g.current_user, permission)
|
||||
|
||||
|
||||
def portfolio():
|
||||
if g.current_user is None:
|
||||
return {}
|
||||
elif g.portfolio is not None:
|
||||
active_task_orders = [
|
||||
task_order for task_order in g.portfolio.task_orders if task_order.is_active
|
||||
]
|
||||
funding_end_date = (
|
||||
# TODO: fix task order -- reimplement logic to get end date from CLINs
|
||||
# sorted(active_task_orders, key=attrgetter("end_date"))[-1].end_date
|
||||
# if active_task_orders
|
||||
# else None
|
||||
None
|
||||
)
|
||||
funded = len(active_task_orders) > 1
|
||||
else:
|
||||
funding_end_date = None
|
||||
funded = None
|
||||
|
||||
return {
|
||||
"portfolio": g.portfolio,
|
||||
"permissions": Permissions,
|
||||
"user_can": user_can_view,
|
||||
"funding_end_date": funding_end_date,
|
||||
"funded": funded,
|
||||
}
|
||||
|
||||
|
||||
def atat():
|
||||
return {"permissions": Permissions, "user_can": user_can_view}
|
208
atat/utils/flash.py
Normal file
208
atat/utils/flash.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from flask import flash
|
||||
from atat.utils.localization import translate
|
||||
|
||||
|
||||
MESSAGES = {
|
||||
"application_created": {
|
||||
"title": "flash.application.created.title",
|
||||
"message": "flash.application.created.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_updated": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.application.updated",
|
||||
"category": "success",
|
||||
},
|
||||
"application_environments_name_error": {
|
||||
"title": None,
|
||||
"message": "flash.application.env_name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_environments_updated": {
|
||||
"title": "flash.environment.updated.title",
|
||||
"message": "flash.environment.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_error": {
|
||||
"title": "flash.application_invite.error.title",
|
||||
"message": "flash.application_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_invite_resent": {
|
||||
"title": None,
|
||||
"message": "flash.application_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_removed": {
|
||||
"title": "flash.application_member.removed.title",
|
||||
"message": "flash.application_member.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_update_error": {
|
||||
"title": "flash.application_member.update_error.title",
|
||||
"message": "flash.application_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_member_updated": {
|
||||
"title": "flash.application_member.updated.title",
|
||||
"message": "flash.application_member.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_name_error": {
|
||||
"title": None,
|
||||
"message": "flash.application.name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"ccpo_user_added": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.added.message",
|
||||
"category": "success",
|
||||
},
|
||||
"ccpo_user_not_found": {
|
||||
"title": "ccpo.form.user_not_found_title",
|
||||
"message": "ccpo.form.user_not_found_text",
|
||||
"category": "info",
|
||||
},
|
||||
"ccpo_user_removed": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_added": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.environment_added",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_deleted": {
|
||||
"title": "flash.environment.deleted.title",
|
||||
"message": "flash.environment.deleted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_subscription_failure": {
|
||||
"title": "flash.environment.subscription_failure.title",
|
||||
"message": "flash.environment.subscription_failure.message",
|
||||
"category": "error",
|
||||
},
|
||||
"environment_subscription_success": {
|
||||
"title": "flash.environment.subscription_success.title",
|
||||
"message": "flash.environment.subscription_success.message",
|
||||
"category": "success",
|
||||
},
|
||||
"form_errors": {
|
||||
"title": "flash.form.errors.title",
|
||||
"message": "flash.form.errors.message",
|
||||
"category": "error",
|
||||
},
|
||||
"insufficient_funds": {
|
||||
"title": "flash.task_order.insufficient_funds.title",
|
||||
"message": None,
|
||||
"category": "warning",
|
||||
},
|
||||
"invite_revoked": {
|
||||
"title": "flash.invite_revoked.title",
|
||||
"message": "flash.invite_revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"logged_out": {
|
||||
"title": "flash.logged_out.title",
|
||||
"message": "flash.logged_out.message",
|
||||
"category": "info",
|
||||
},
|
||||
"login_next": {
|
||||
"title": "flash.login_required_title",
|
||||
"message": "flash.login_required_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"new_application_member": {
|
||||
"title": "flash.new_application_member.title",
|
||||
"message": "flash.new_application_member.message",
|
||||
"category": "success",
|
||||
},
|
||||
"new_portfolio_member": {
|
||||
"title": "flash.new_portfolio_member.title",
|
||||
"message": "flash.new_portfolio_member.message",
|
||||
"category": "success",
|
||||
},
|
||||
"portfolio_member_removed": {
|
||||
"title": "flash.deleted_member",
|
||||
"message": "flash.delete_member_success",
|
||||
"category": "success",
|
||||
},
|
||||
"primary_point_of_contact_changed": {
|
||||
"title": "flash.new_ppoc_title",
|
||||
"message": "flash.new_ppoc_message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation": {
|
||||
"title": None,
|
||||
"message": "flash.portfolio_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation_error": {
|
||||
"title": "flash.portfolio_invite.error.title",
|
||||
"message": "flash.portfolio_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"revoked_portfolio_access": {
|
||||
"title": "flash.portfolio_member.revoked.title",
|
||||
"message": "flash.portfolio_member.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"session_expired": {
|
||||
"title": "flash.session_expired.title",
|
||||
"message": "flash.session_expired.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_draft": {
|
||||
"title": "task_orders.form.draft_alert_title",
|
||||
"message": "task_orders.form.draft_alert_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"task_order_number_error": {
|
||||
"title": None,
|
||||
"message": "flash.task_order_number_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_submitted": {
|
||||
"title": "flash.task_order.submitted.title",
|
||||
"message": "flash.task_order.submitted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member": {
|
||||
"title": "flash.portfolio_member.update.title",
|
||||
"message": "flash.portfolio_member.update.message",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_member_error": {
|
||||
"title": "flash.portfolio_member.update_error.title",
|
||||
"message": "flash.portfolio_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"updated_application_team_settings": {
|
||||
"title": "flash.success",
|
||||
"message": "flash.updated_application_team_settings",
|
||||
"category": "success",
|
||||
},
|
||||
"user_must_complete_profile": {
|
||||
"title": "flash.user.complete_profile.title",
|
||||
"message": "flash.user.complete_profile.message",
|
||||
"category": "info",
|
||||
},
|
||||
"user_updated": {
|
||||
"title": "flash.user.updated.title",
|
||||
"message": None,
|
||||
"category": "success",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def formatted_flash(message_name, **message_args):
|
||||
config = MESSAGES[message_name]
|
||||
|
||||
title = translate(config["title"], message_args) if config["title"] else None
|
||||
message = translate(config["message"], message_args) if config["message"] else None
|
||||
actions = (
|
||||
translate(config["actions"], message_args) if config.get("actions") else None
|
||||
)
|
||||
|
||||
flash({"title": title, "message": message, "actions": actions}, config["category"])
|
40
atat/utils/form_cache.py
Normal file
40
atat/utils/form_cache.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
|
||||
DEFAULT_CACHE_NAME = "formcache"
|
||||
|
||||
|
||||
class FormCache(object):
|
||||
PARAM_NAME = "formCache"
|
||||
|
||||
def __init__(self, redis):
|
||||
self.redis = redis
|
||||
|
||||
def from_request(self, http_request):
|
||||
cache_key = http_request.args.get(self.PARAM_NAME)
|
||||
if cache_key:
|
||||
return self.read(cache_key)
|
||||
return MultiDict()
|
||||
|
||||
def write(self, formdata, expiry_seconds=3600, key_prefix=DEFAULT_CACHE_NAME):
|
||||
value = json.dumps(formdata)
|
||||
hash_ = self._hash()
|
||||
self.redis.setex(
|
||||
name=self._key(key_prefix, hash_), value=value, time=expiry_seconds
|
||||
)
|
||||
return hash_
|
||||
|
||||
def read(self, formdata_key, key_prefix=DEFAULT_CACHE_NAME):
|
||||
data = self.redis.get(self._key(key_prefix, formdata_key))
|
||||
dict_data = json.loads(data) if data is not None else {}
|
||||
return MultiDict(dict_data)
|
||||
|
||||
@staticmethod
|
||||
def _key(prefix, hash_):
|
||||
return "{}:{}".format(prefix, hash_)
|
||||
|
||||
@staticmethod
|
||||
def _hash():
|
||||
return sha256().hexdigest()
|
28
atat/utils/json.py
Normal file
28
atat/utils/json.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from flask.json import JSONEncoder
|
||||
import json
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
|
||||
from atat.models.attachment import Attachment
|
||||
|
||||
|
||||
class CustomJSONEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Attachment):
|
||||
return obj.filename
|
||||
elif isinstance(obj, date):
|
||||
return obj.strftime("%Y-%m-%d")
|
||||
elif isinstance(obj, FileStorage):
|
||||
return obj.filename
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def sqlalchemy_dumps(dct):
|
||||
def _default(obj):
|
||||
if isinstance(obj, Enum):
|
||||
return obj.name
|
||||
else:
|
||||
raise TypeError()
|
||||
|
||||
return json.dumps(dct, default=_default)
|
56
atat/utils/localization.py
Normal file
56
atat/utils/localization.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import yaml
|
||||
from functools import lru_cache
|
||||
from flask import current_app as app
|
||||
from atat.utils import getattr_path
|
||||
|
||||
|
||||
class LocalizationInvalidKeyError(Exception):
|
||||
def __init__(self, key, variables):
|
||||
self.key = key
|
||||
self.variables = variables
|
||||
|
||||
def __str__(self):
|
||||
return "Requested {key} and variables {variables} with but an error occured".format(
|
||||
key=self.key, variables=self.variables
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _translations_file():
|
||||
file_name = "translations.yaml"
|
||||
|
||||
if app:
|
||||
file_name = app.config.get("DEFAULT_TRANSLATIONS_FILE", file_name)
|
||||
|
||||
f = open(file_name)
|
||||
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def all_keys():
|
||||
translations = _translations_file()
|
||||
keys = []
|
||||
|
||||
def _recursive_key_lookup(chain):
|
||||
results = getattr_path(translations, chain)
|
||||
if isinstance(results, str):
|
||||
keys.append(chain)
|
||||
else:
|
||||
[_recursive_key_lookup(".".join([chain, result])) for result in results]
|
||||
|
||||
[_recursive_key_lookup(key) for key in translations]
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def translate(key, variables=None):
|
||||
translations = _translations_file()
|
||||
value = getattr_path(translations, key)
|
||||
|
||||
if variables is None:
|
||||
variables = {}
|
||||
|
||||
if value is None:
|
||||
raise LocalizationInvalidKeyError(key, variables)
|
||||
|
||||
return value.format(**variables).replace("\n", "")
|
68
atat/utils/logging.py
Normal file
68
atat/utils/logging.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import g, request, has_request_context, session
|
||||
|
||||
|
||||
class RequestContextFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if has_request_context():
|
||||
if getattr(g, "current_user", None):
|
||||
record.dod_edipi = g.current_user.dod_id
|
||||
|
||||
user_id = session.get("user_id")
|
||||
if user_id:
|
||||
record.user_id = str(user_id)
|
||||
record.logged_in = True
|
||||
else:
|
||||
record.logged_in = False
|
||||
|
||||
if request.environ.get("HTTP_X_REQUEST_ID"):
|
||||
record.request_id = request.environ.get("HTTP_X_REQUEST_ID")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def epoch_to_iso8601(ts):
|
||||
dt = datetime.datetime.utcfromtimestamp(ts)
|
||||
return dt.replace(tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
_DEFAULT_RECORD_FIELDS = [
|
||||
("timestamp", lambda r: epoch_to_iso8601(r.created)),
|
||||
("version", lambda r: 1),
|
||||
("request_id", lambda r: r.__dict__.get("request_id")),
|
||||
("user_id", lambda r: r.__dict__.get("user_id")),
|
||||
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
|
||||
("logged_in", lambda r: r.__dict__.get("logged_in")),
|
||||
("severity", lambda r: r.levelname),
|
||||
("tags", lambda r: r.__dict__.get("tags")),
|
||||
("audit_event", lambda r: r.__dict__.get("audit_event")),
|
||||
]
|
||||
|
||||
def __init__(self, *args, source="atat", **kwargs):
|
||||
self.source = source
|
||||
super().__init__(self)
|
||||
|
||||
def format(self, record, *args, **kwargs):
|
||||
message_dict = {"source": self.source}
|
||||
|
||||
for field, func in self._DEFAULT_RECORD_FIELDS:
|
||||
result = func(record)
|
||||
if result is not None:
|
||||
message_dict[field] = result
|
||||
|
||||
if record.args:
|
||||
message_dict["message"] = record.msg % record.args
|
||||
else:
|
||||
message_dict["message"] = record.msg
|
||||
|
||||
if record.__dict__.get("exc_info") is not None:
|
||||
message_dict["details"] = {
|
||||
"backtrace": self.formatException(record.exc_info),
|
||||
"exception": str(record.exc_info[1]),
|
||||
}
|
||||
|
||||
return json.dumps(message_dict)
|
112
atat/utils/mailer.py
Normal file
112
atat/utils/mailer.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from contextlib import contextmanager
|
||||
import smtplib
|
||||
import io
|
||||
from email.message import EmailMessage
|
||||
|
||||
|
||||
class MailConnection(object):
|
||||
def send(self, message):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SMTPConnection(MailConnection):
|
||||
def __init__(self, server, port, username, password, use_tls=False):
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.use_tls = use_tls
|
||||
|
||||
@contextmanager
|
||||
def _connected_host(self):
|
||||
host = None
|
||||
|
||||
if self.use_tls:
|
||||
host = smtplib.SMTP(self.server, self.port)
|
||||
host.starttls()
|
||||
else:
|
||||
host = smtplib.SMTP_SSL(self.server, self.port)
|
||||
|
||||
host.login(self.username, self.password)
|
||||
|
||||
yield host
|
||||
|
||||
host.quit()
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
return []
|
||||
|
||||
def send(self, message):
|
||||
with self._connected_host() as host:
|
||||
host.send_message(message)
|
||||
|
||||
|
||||
class RedisConnection(MailConnection):
|
||||
def __init__(self, redis, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.redis = redis
|
||||
self._reset()
|
||||
|
||||
def _reset(self):
|
||||
self.redis.delete("atat_inbox")
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
return [msg.decode() for msg in self.redis.lrange("atat_inbox", 0, -1)]
|
||||
|
||||
def send(self, message):
|
||||
self.redis.lpush("atat_inbox", str(message))
|
||||
|
||||
|
||||
class Mailer(object):
|
||||
def __init__(self, connection, sender):
|
||||
self.connection = connection
|
||||
self.sender = sender
|
||||
|
||||
def _build_message(self, recipients, subject, body):
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
msg["From"] = self.sender
|
||||
msg["To"] = ", ".join(recipients)
|
||||
msg["Subject"] = subject
|
||||
|
||||
return msg
|
||||
|
||||
def _add_attachment(self, message, content, filename, maintype, subtype):
|
||||
with io.BytesIO(content) as bytes_:
|
||||
message.add_attachment(
|
||||
bytes_.read(), filename=filename, maintype=maintype, subtype=subtype
|
||||
)
|
||||
|
||||
def send(self, recipients, subject, body, attachments=[]):
|
||||
"""
|
||||
Send a message, optionally with attachments.
|
||||
Attachments should be provided as a list of dictionaries of the form:
|
||||
{
|
||||
content: bytes,
|
||||
maintype: string,
|
||||
subtype: string,
|
||||
filename: string,
|
||||
}
|
||||
"""
|
||||
message = self._build_message(recipients, subject, body)
|
||||
if attachments:
|
||||
message.make_mixed()
|
||||
for attachment in attachments:
|
||||
self._add_attachment(
|
||||
message,
|
||||
content=attachment["content"],
|
||||
filename=attachment["filename"],
|
||||
maintype=attachment.get("maintype", "application"),
|
||||
subtype=attachment.get("subtype", "octet-stream"),
|
||||
)
|
||||
self.connection.send(message)
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
return self.connection.messages
|
17
atat/utils/notification_sender.py
Normal file
17
atat/utils/notification_sender.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import select
|
||||
|
||||
from atat.jobs import send_notification_mail
|
||||
from atat.database import db
|
||||
from atat.models.notification_recipient import NotificationRecipient
|
||||
|
||||
|
||||
class NotificationSender(object):
|
||||
EMAIL_SUBJECT = "ATAT notification"
|
||||
|
||||
def send(self, body, type_=None):
|
||||
recipients = self._get_recipients(type_)
|
||||
send_notification_mail.delay(recipients, self.EMAIL_SUBJECT, body)
|
||||
|
||||
def _get_recipients(self, type_):
|
||||
query = select([NotificationRecipient.email])
|
||||
return db.session.execute(query).fetchone()
|
20
atat/utils/session_limiter.py
Normal file
20
atat/utils/session_limiter.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from atat.domain.users import Users
|
||||
|
||||
|
||||
class SessionLimiter(object):
|
||||
def __init__(self, config, session, redis):
|
||||
self.limit_logins = config["LIMIT_CONCURRENT_SESSIONS"]
|
||||
self.session_prefix = config.get("SESSION_KEY_PREFIX", "session:")
|
||||
self.session = session
|
||||
self.redis = redis
|
||||
|
||||
def on_login(self, user):
|
||||
if not self.limit_logins:
|
||||
return
|
||||
|
||||
session_id = self.session.sid
|
||||
self._delete_session(user.last_session_id)
|
||||
Users.update_last_session_id(user, session_id)
|
||||
|
||||
def _delete_session(self, session_id):
|
||||
self.redis.delete(f"{self.session_prefix}{session_id}")
|
Reference in New Issue
Block a user