feat: Tags (#39)
parent
ca2e127b01
commit
0159c50742
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.19.0
|
||||||
|
|
||||||
|
* Added instance Tags. Instance owners can add and remove them
|
||||||
|
* Limited retrieval of whitelist to 10 instances by default
|
||||||
|
* Added paging and limit to whitelist get
|
||||||
|
* Instances will now display their tags, unless muted.
|
||||||
|
* Can now retrieve instances in the whitelist filtered by tags
|
||||||
|
* Fix hesitations received appearing when visibility was limited.
|
||||||
|
|
||||||
# 0.18.0
|
# 0.18.0
|
||||||
|
|
||||||
* Added instance flags
|
* Added instance flags
|
||||||
|
|
|
@ -41,6 +41,8 @@ class Models:
|
||||||
'sysadmins': fields.Integer(required=False, default=None, description="The count of system administrators in this instance as reported by its admins."),
|
'sysadmins': fields.Integer(required=False, default=None, description="The count of system administrators in this instance as reported by its admins."),
|
||||||
'moderators': fields.Integer(required=False, default=None, description="The count of community moderators in this instance as reported by its admins."),
|
'moderators': fields.Integer(required=False, default=None, description="The count of community moderators in this instance as reported by its admins."),
|
||||||
'state': fields.String(required=True, enum=[e.name for e in enums.InstanceState], description="The state of the instance as seen from the fediseer."),
|
'state': fields.String(required=True, enum=[e.name for e in enums.InstanceState], description="The state of the instance as seen from the fediseer."),
|
||||||
|
'tags': fields.List(fields.String(min_length=2, required=False, description="Domain tags (if any)")),
|
||||||
|
|
||||||
})
|
})
|
||||||
self.response_model_flag_details = api.model('FlagDetails', {
|
self.response_model_flag_details = api.model('FlagDetails', {
|
||||||
'flag': fields.String(required=True, enum=[e.name for e in enums.InstanceFlags], description="The type of flag"),
|
'flag': fields.String(required=True, enum=[e.name for e in enums.InstanceFlags], description="The type of flag"),
|
||||||
|
@ -135,3 +137,6 @@ class Models:
|
||||||
'flag': fields.String(required=True, enum=[e.name for e in enums.InstanceFlags], description="The type of flag to apply"),
|
'flag': fields.String(required=True, enum=[e.name for e in enums.InstanceFlags], description="The type of flag to apply"),
|
||||||
'comment': fields.String(max_length=255, required=False, description="A comment explaining this flag", example="reasons"),
|
'comment': fields.String(max_length=255, required=False, description="A comment explaining this flag", example="reasons"),
|
||||||
})
|
})
|
||||||
|
self.input_tags = api.model('Tags', {
|
||||||
|
'tags_csv': fields.String(min_length=2, required=True, description="A comma-separated list of tags"),
|
||||||
|
})
|
||||||
|
|
|
@ -10,6 +10,7 @@ import fediseer.apis.v1.badges as badges
|
||||||
import fediseer.apis.v1.find as find
|
import fediseer.apis.v1.find as find
|
||||||
import fediseer.apis.v1.report as report
|
import fediseer.apis.v1.report as report
|
||||||
import fediseer.apis.v1.admin as admin
|
import fediseer.apis.v1.admin as admin
|
||||||
|
import fediseer.apis.v1.tags as tags
|
||||||
from fediseer.apis.v1.base import api
|
from fediseer.apis.v1.base import api
|
||||||
|
|
||||||
api.add_resource(base.Suspicions, "/instances")
|
api.add_resource(base.Suspicions, "/instances")
|
||||||
|
@ -30,4 +31,5 @@ api.add_resource(guarantees.Guarantees, "/guarantees/<string:domain>")
|
||||||
api.add_resource(badges.GuaranteeBadge, "/badges/guarantees/<string:domain>.svg")
|
api.add_resource(badges.GuaranteeBadge, "/badges/guarantees/<string:domain>.svg")
|
||||||
api.add_resource(badges.EndorsementBadge, "/badges/endorsements/<string:domain>.svg")
|
api.add_resource(badges.EndorsementBadge, "/badges/endorsements/<string:domain>.svg")
|
||||||
api.add_resource(admin.Flag, "/admin/flags/<string:domain>")
|
api.add_resource(admin.Flag, "/admin/flags/<string:domain>")
|
||||||
|
api.add_resource(tags.Tags, "/admin/tags")
|
||||||
api.add_resource(report.Report, "/reports")
|
api.add_resource(report.Report, "/reports")
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from fediseer.apis.v1.base import *
|
from fediseer.apis.v1.base import *
|
||||||
from fediseer.messaging import activitypub_pm
|
|
||||||
from fediseer import enums
|
from fediseer import enums
|
||||||
from fediseer.classes.reports import Report
|
from fediseer.classes.reports import Report
|
||||||
from fediseer.register import ensure_instance_registered
|
from fediseer.register import ensure_instance_registered
|
||||||
|
|
|
@ -13,6 +13,8 @@ class Approvals(Resource):
|
||||||
get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args")
|
get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args")
|
||||||
get_parser.add_argument("min_endorsements", required=False, default=1, type=int, help="Limit to this amount of endorsements of more", location="args")
|
get_parser.add_argument("min_endorsements", required=False, default=1, type=int, help="Limit to this amount of endorsements of more", location="args")
|
||||||
get_parser.add_argument("reasons_csv", required=False, type=str, help="Only retrieve endorsements where their reasons include any of the text in this csv", location="args")
|
get_parser.add_argument("reasons_csv", required=False, type=str, help="Only retrieve endorsements where their reasons include any of the text in this csv", location="args")
|
||||||
|
get_parser.add_argument("page", required=False, type=int, default=1, help="Which page of results to retrieve", location="args")
|
||||||
|
get_parser.add_argument("limit", required=False, type=int, default=10, help="Which page of results to retrieve", location="args")
|
||||||
|
|
||||||
decorators = [limiter.limit("45/minute"), limiter.limit("30/minute", key_func = get_request_path)]
|
decorators = [limiter.limit("45/minute"), limiter.limit("30/minute", key_func = get_request_path)]
|
||||||
@api.expect(get_parser)
|
@api.expect(get_parser)
|
||||||
|
|
|
@ -113,12 +113,12 @@ class Hesitations(Resource):
|
||||||
precheck_instances = database.get_all_hesitant_instances_by_dubious_id(instance.id)
|
precheck_instances = database.get_all_hesitant_instances_by_dubious_id(instance.id)
|
||||||
instances = []
|
instances = []
|
||||||
for p_instance in precheck_instances:
|
for p_instance in precheck_instances:
|
||||||
if p_instance.visibility_endorsements == enums.ListVisibility.ENDORSED:
|
if p_instance.visibility_hesitations == enums.ListVisibility.ENDORSED:
|
||||||
if get_instance is None:
|
if get_instance is None:
|
||||||
continue
|
continue
|
||||||
if not p_instance.is_endorsing(get_instance):
|
if not p_instance.is_endorsing(get_instance):
|
||||||
continue
|
continue
|
||||||
if p_instance.visibility_endorsements == enums.ListVisibility.PRIVATE:
|
if p_instance.visibility_hesitations == enums.ListVisibility.PRIVATE:
|
||||||
if get_instance is None:
|
if get_instance is None:
|
||||||
continue
|
continue
|
||||||
if p_instance != get_instance:
|
if p_instance != get_instance:
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
from fediseer.apis.v1.base import *
|
||||||
|
from fediseer import enums
|
||||||
|
from fediseer.classes.reports import Report
|
||||||
|
from fediseer.classes.instance import InstanceTag
|
||||||
|
|
||||||
|
class Tags(Resource):
|
||||||
|
|
||||||
|
put_parser = reqparse.RequestParser()
|
||||||
|
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
|
||||||
|
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
|
||||||
|
put_parser.add_argument("tags_csv", required=True, type=str, help="The tags to apply", location="json")
|
||||||
|
|
||||||
|
|
||||||
|
@api.expect(put_parser,models.input_tags, validate=True)
|
||||||
|
@api.marshal_with(models.response_model_simple_response, code=200, description='Action Result')
|
||||||
|
@api.response(400, 'Bad Request', models.response_model_error)
|
||||||
|
@api.response(401, 'Invalid API Key', models.response_model_error)
|
||||||
|
@api.response(403, 'Access Denied', models.response_model_error)
|
||||||
|
|
||||||
|
def put(self):
|
||||||
|
'''Tag your instance.
|
||||||
|
No hate speech allowed!
|
||||||
|
'''
|
||||||
|
self.args = self.put_parser.parse_args()
|
||||||
|
if not self.args.apikey:
|
||||||
|
raise e.Unauthorized("You must provide the API key that was PM'd to the admin account of this instance")
|
||||||
|
user = database.find_user_by_api_key(self.args.apikey)
|
||||||
|
if not user:
|
||||||
|
raise e.Forbidden("Instance not found. Have you remembed to claim it?")
|
||||||
|
instance = database.find_instance_by_user(user)
|
||||||
|
changed = False
|
||||||
|
tags = [t.strip() for t in self.args.tags_csv.split(',')]
|
||||||
|
if database.instance_has_flag(instance.id,enums.InstanceFlags.RESTRICTED):
|
||||||
|
raise e.Forbidden("You cannot take this action as your instance is restricted")
|
||||||
|
if database.count_instance_tags(instance.id) + len(tags) >= 100:
|
||||||
|
raise e.BadRequest("You can't have more than 100 tags")
|
||||||
|
if len(tags) != len(set([t.lower() for t in tags])):
|
||||||
|
raise e.BadRequest("You cannot specify the same tag with different case.")
|
||||||
|
for tag in tags:
|
||||||
|
if database.instance_has_tag(instance.id,tag):
|
||||||
|
continue
|
||||||
|
new_tag = InstanceTag(
|
||||||
|
instance_id = instance.id,
|
||||||
|
tag = tag,
|
||||||
|
)
|
||||||
|
db.session.add(new_tag)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
db.session.commit()
|
||||||
|
return {"message": "Changed"},200
|
||||||
|
return {"message": "OK"},200
|
||||||
|
|
||||||
|
delete_parser = reqparse.RequestParser()
|
||||||
|
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
|
||||||
|
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
|
||||||
|
delete_parser.add_argument("tags_csv", required=True, type=str, help="The tags to delete", location="json")
|
||||||
|
|
||||||
|
|
||||||
|
@api.expect(delete_parser,models.input_tags)
|
||||||
|
@api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True)
|
||||||
|
@api.response(400, 'Bad Request', models.response_model_error)
|
||||||
|
@api.response(401, 'Invalid API Key', models.response_model_error)
|
||||||
|
@api.response(403, 'Forbidden', models.response_model_error)
|
||||||
|
def delete(self):
|
||||||
|
'''Delete an instance's tag
|
||||||
|
'''
|
||||||
|
self.args = self.patch_parser.parse_args()
|
||||||
|
if not self.args.apikey:
|
||||||
|
raise e.Unauthorized("You must provide the API key that was PM'd to the admin account")
|
||||||
|
user = database.find_user_by_api_key(self.args.apikey)
|
||||||
|
if not user:
|
||||||
|
raise e.Forbidden("Instance not found. Have you remembed to claim it?")
|
||||||
|
instance = database.find_instance_by_user(user)
|
||||||
|
changed = False
|
||||||
|
tags = [t.strip() for t in self.args.tags_csv.split(',')]
|
||||||
|
for tag in tags:
|
||||||
|
existing_tag = database.get_instance_tag(instance.id,tag)
|
||||||
|
if not existing_tag:
|
||||||
|
existing_tag
|
||||||
|
db.session.delete(existing_tag)
|
||||||
|
if changed:
|
||||||
|
db.session.commit()
|
||||||
|
return {"message": "Changed"},200
|
||||||
|
return {"message": "OK"},200
|
|
@ -11,6 +11,9 @@ class Whitelist(Resource):
|
||||||
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
|
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
|
||||||
get_parser.add_argument("endorsements", required=False, default=0, type=int, help="Limit to this amount of endorsements of more", location="args")
|
get_parser.add_argument("endorsements", required=False, default=0, type=int, help="Limit to this amount of endorsements of more", location="args")
|
||||||
get_parser.add_argument("guarantors", required=False, default=1, type=int, help="Limit to this amount of guarantors of more", location="args")
|
get_parser.add_argument("guarantors", required=False, default=1, type=int, help="Limit to this amount of guarantors of more", location="args")
|
||||||
|
get_parser.add_argument("tags_csv", required=False, type=str, help="A list of tags to filter.", location="args")
|
||||||
|
get_parser.add_argument("page", required=False, type=int, default=1, help="Which page of results to retrieve", location="args")
|
||||||
|
get_parser.add_argument("limit", required=False, type=int, default=1000, help="Which page of results to retrieve", location="args")
|
||||||
get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args")
|
get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args")
|
||||||
get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args")
|
get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args")
|
||||||
|
|
||||||
|
@ -21,8 +24,21 @@ class Whitelist(Resource):
|
||||||
'''A List with the details of all instances and their endorsements
|
'''A List with the details of all instances and their endorsements
|
||||||
'''
|
'''
|
||||||
self.args = self.get_parser.parse_args()
|
self.args = self.get_parser.parse_args()
|
||||||
|
# if self.args.limit > 100: # Once limit is in effect
|
||||||
|
# raise e.BadRequest("limit cannot be more than 100")
|
||||||
|
if self.args.limit < 10:
|
||||||
|
raise e.BadRequest("Limit cannot be less than 10")
|
||||||
|
tags = None
|
||||||
|
if self.args.tags_csv is not None:
|
||||||
|
tags = [t.strip() for t in self.args.tags_csv.split(',')]
|
||||||
instance_details = []
|
instance_details = []
|
||||||
for instance in database.get_all_instances(self.args.endorsements,self.args.guarantors):
|
for instance in database.get_all_instances(
|
||||||
|
min_endorsements=self.args.endorsements,
|
||||||
|
min_guarantors=self.args.guarantors,
|
||||||
|
tags=tags,
|
||||||
|
page=self.args.page,
|
||||||
|
limit=self.args.limit,
|
||||||
|
):
|
||||||
instance_details.append(instance.get_details(show_visibilities=True))
|
instance_details.append(instance.get_details(show_visibilities=True))
|
||||||
if self.args.csv:
|
if self.args.csv:
|
||||||
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
|
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import Enum, UniqueConstraint
|
from sqlalchemy import Enum, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy import func, Index
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from fediseer.flask import db, SQLITE_MODE
|
from fediseer.flask import db, SQLITE_MODE
|
||||||
|
@ -92,6 +93,18 @@ class InstanceFlag(db.Model):
|
||||||
instance = db.relationship("Instance", back_populates="flags")
|
instance = db.relationship("Instance", back_populates="flags")
|
||||||
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
|
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||||
|
|
||||||
|
class InstanceTag(db.Model):
|
||||||
|
__tablename__ = "instance_tags"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('instance_id', 'tag', name='instance_tags_instance_id_tag'),
|
||||||
|
Index("ix_tags_lower", func.lower('tag')),
|
||||||
|
)
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
tag = db.Column(db.String(100), unique=False, nullable=False)
|
||||||
|
instance_id = db.Column(db.Integer, db.ForeignKey("instances.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
instance = db.relationship("Instance", back_populates="tags")
|
||||||
|
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
class Instance(db.Model):
|
class Instance(db.Model):
|
||||||
__tablename__ = "instances"
|
__tablename__ = "instances"
|
||||||
|
|
||||||
|
@ -127,7 +140,8 @@ class Instance(db.Model):
|
||||||
rejections = db.relationship("RejectionRecord", back_populates="rejector_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejector_id])
|
rejections = db.relationship("RejectionRecord", back_populates="rejector_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejector_id])
|
||||||
rejectors = db.relationship("RejectionRecord", back_populates="rejected_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejected_id])
|
rejectors = db.relationship("RejectionRecord", back_populates="rejected_instance", cascade="all, delete-orphan", foreign_keys=[RejectionRecord.rejected_id])
|
||||||
admins = db.relationship("Claim", back_populates="instance", cascade="all, delete-orphan")
|
admins = db.relationship("Claim", back_populates="instance", cascade="all, delete-orphan")
|
||||||
flags = db.relationship("InstanceFlag", back_populates="instance", cascade="all, delete-orphan")
|
flags = db.relationship("InstanceFlag", back_populates="instance", cascade="all, delete-orphan", lazy='joined')
|
||||||
|
tags = db.relationship("InstanceTag", back_populates="instance", cascade="all, delete-orphan", lazy='joined')
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
|
@ -153,6 +167,7 @@ class Instance(db.Model):
|
||||||
"sysadmins": self.sysadmins,
|
"sysadmins": self.sysadmins,
|
||||||
"moderators": self.moderators,
|
"moderators": self.moderators,
|
||||||
"state": self.get_state().name,
|
"state": self.get_state().name,
|
||||||
|
"tags": [t.tag for t in self.tags] if not self.has_flag(enums.InstanceFlags.MUTED) else [],
|
||||||
}
|
}
|
||||||
if show_visibilities:
|
if show_visibilities:
|
||||||
ret_dict["visibility_endorsements"] = self.visibility_endorsements.name
|
ret_dict["visibility_endorsements"] = self.visibility_endorsements.name
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FEDISEER_VERSION = "0.18.0"
|
FEDISEER_VERSION = "0.19.0"
|
||||||
SUPPORTED_SOFTWARE = {
|
SUPPORTED_SOFTWARE = {
|
||||||
"lemmy",
|
"lemmy",
|
||||||
"mastodon",
|
"mastodon",
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import func, or_, and_, not_, Boolean
|
from sqlalchemy import func, or_, and_, not_, Boolean
|
||||||
from sqlalchemy.orm import noload
|
from fediseer.flask import db
|
||||||
from fediseer.flask import db, SQLITE_MODE
|
|
||||||
from fediseer.utils import hash_api_key
|
from fediseer.utils import hash_api_key
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure, Hesitation, Solicitation, InstanceFlag
|
from fediseer.classes.instance import Instance, Endorsement, Guarantee, RejectionRecord, Censure, Hesitation, Solicitation, InstanceFlag, InstanceTag
|
||||||
from fediseer.classes.user import Claim, User
|
from fediseer.classes.user import Claim, User
|
||||||
from fediseer.classes.reports import Report
|
from fediseer.classes.reports import Report
|
||||||
from fediseer import enums
|
from fediseer import enums
|
||||||
from fediseer.consts import POLLS_PER_DAY
|
|
||||||
|
|
||||||
def get_all_instances(min_endorsements = 0, min_guarantors = 1, include_decommissioned = True):
|
def get_all_instances(
|
||||||
|
min_endorsements = 0,
|
||||||
|
min_guarantors = 1,
|
||||||
|
tags = None,
|
||||||
|
include_decommissioned = True,
|
||||||
|
page=1,
|
||||||
|
limit=10,
|
||||||
|
):
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
Instance
|
Instance
|
||||||
).outerjoin(
|
).outerjoin(
|
||||||
|
@ -35,8 +37,14 @@ def get_all_instances(min_endorsements = 0, min_guarantors = 1, include_decommis
|
||||||
).having(
|
).having(
|
||||||
db.func.count(Instance.guarantors) >= min_guarantors,
|
db.func.count(Instance.guarantors) >= min_guarantors,
|
||||||
)
|
)
|
||||||
return query.all()
|
if tags:
|
||||||
|
# Convert tags to lowercase and filter instances that have any of the tags
|
||||||
|
lower_tags = [tag.lower() for tag in tags]
|
||||||
|
query = query.filter(Instance.tags.any(func.lower(InstanceTag.tag).in_(lower_tags)))
|
||||||
|
page -= 1
|
||||||
|
if page < 0:
|
||||||
|
page = 0
|
||||||
|
return query.order_by(Instance.created.desc()).offset(limit * page).limit(limit).all()
|
||||||
|
|
||||||
def get_all_endorsed_instances_by_approving_id(approving_ids):
|
def get_all_endorsed_instances_by_approving_id(approving_ids):
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
|
@ -474,3 +482,23 @@ def instance_has_flag(instance_id, flag_enum):
|
||||||
InstanceFlag.flag == flag_enum,
|
InstanceFlag.flag == flag_enum,
|
||||||
)
|
)
|
||||||
return query.count() == 1
|
return query.count() == 1
|
||||||
|
|
||||||
|
def get_instance_tag(instance_id, tag: str):
|
||||||
|
query = InstanceTag.query.filter(
|
||||||
|
InstanceTag.instance_id == instance_id,
|
||||||
|
InstanceTag.flag == tag,
|
||||||
|
)
|
||||||
|
return query.first()
|
||||||
|
|
||||||
|
def instance_has_tag(instance_id, tag: str):
|
||||||
|
query = InstanceTag.query.filter(
|
||||||
|
InstanceTag.instance_id == instance_id,
|
||||||
|
InstanceTag.tag == tag.lower(),
|
||||||
|
)
|
||||||
|
return query.count() == 1
|
||||||
|
|
||||||
|
def count_instance_tags(instance_id):
|
||||||
|
query = InstanceTag.query.filter(
|
||||||
|
InstanceTag.instance_id == instance_id,
|
||||||
|
)
|
||||||
|
return query.count()
|
Loading…
Reference in New Issue