Merge branch 'create-shout-2' into 'main'

New create shout flow

See merge request discoursio/discoursio-api!17
This commit is contained in:
Igor 2023-05-03 15:48:22 +00:00
commit c11c862d96
9 changed files with 98 additions and 292 deletions

19
migrate.sh Normal file
View File

@ -0,0 +1,19 @@
database_name="discoursio"
echo "DATABASE MIGRATION STARTED"
echo "Dropping database $database_name"
dropdb $database_name --force
if [ $? -ne 0 ]; then { echo "Failed to drop database, aborting." ; exit 1; } fi
echo "Database $database_name dropped"
echo "Creating database $database_name"
createdb $database_name
if [ $? -ne 0 ]; then { echo "Failed to create database, aborting." ; exit 1; } fi
echo "Database $database_name successfully created"
echo "Start migration"
python3 server.py migrate
if [ $? -ne 0 ]; then { echo "Migration failed, aborting." ; exit 1; } fi
echo 'Done!'

View File

@ -1,41 +0,0 @@
from datetime import datetime
from sqlalchemy import Boolean, Column, ForeignKey, DateTime, String
from sqlalchemy.orm import relationship
from base.orm import Base
from orm.user import User
from orm.topic import Topic
class DraftTopic(Base):
__tablename__ = "draft_topic"
id = None # type: ignore
collab = Column(ForeignKey("draft_collab.id"), primary_key=True)
topic = Column(ForeignKey("topic.id"), primary_key=True)
main = Column(Boolean, default=False)
class DraftAuthor(Base):
__tablename__ = "draft_author"
id = None # type: ignore
collab = Column(ForeignKey("draft_collab.id"), primary_key=True)
author = Column(ForeignKey("user.id"), primary_key=True)
accepted = Column(Boolean, default=False)
class DraftCollab(Base):
__tablename__ = "draft_collab"
slug = Column(String, nullable=True, comment="Slug")
title = Column(String, nullable=True, comment="Title")
subtitle = Column(String, nullable=True, comment="Subtitle")
layout = Column(String, nullable=True, comment="Layout format")
body = Column(String, nullable=True, comment="Body")
cover = Column(String, nullable=True, comment="Cover")
authors = relationship(lambda: User, secondary=DraftAuthor.__tablename__)
topics = relationship(lambda: Topic, secondary=DraftTopic.__tablename__)
createdAt = Column(DateTime, default=datetime.now, comment="Created At")
updatedAt = Column(DateTime, default=datetime.now, comment="Updated At")
chat = Column(String, unique=True, nullable=True)

View File

@ -48,9 +48,11 @@ class Shout(Base):
publishedAt = Column(DateTime, nullable=True)
deletedAt = Column(DateTime, nullable=True)
# same with Draft
createdBy = Column(ForeignKey("user.id"), comment="Created By")
deletedBy = Column(ForeignKey("user.id"), nullable=True)
slug = Column(String, unique=True)
cover = Column(String, nullable=True, comment="Cover")
cover = Column(String, nullable=True, comment="Cover image url")
body = Column(String, nullable=False, comment="Body")
title = Column(String, nullable=True)
subtitle = Column(String, nullable=True)

View File

@ -8,8 +8,6 @@ from resolvers.auth import (
get_current_user,
)
from resolvers.create.drafts import load_drafts, create_draft, update_draft, delete_draft,\
accept_coauthor, invite_coauthor, draft_to_shout
from resolvers.create.migrate import markdown_body
from resolvers.create.editor import create_shout, delete_shout, update_shout

View File

@ -1,189 +0,0 @@
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import query, mutation
from orm.draft import DraftCollab, DraftAuthor
from orm.shout import Shout
from orm.topic import Topic
from orm.user import User
from datetime import datetime, timezone
from transliterate import translit
import re
@query.field("loadDrafts")
@login_required
async def load_drafts(_, info):
auth: AuthCredentials = info.context["request"].auth
drafts = []
with local_session() as session:
drafts = session.query(DraftCollab).filter(auth.user_id in DraftCollab.authors)
return drafts
@mutation.field("createDraft") # TODO
@login_required
async def create_draft(_, info, draft_input):
auth: AuthCredentials = info.context["request"].auth
draft_input['createdBy'] = auth.user_id
with local_session() as session:
collab = DraftCollab.create(**draft_input)
session.add(collab)
session.commit()
# TODO: email notify to all authors
return {}
@mutation.field("deleteDraft")
@login_required
async def delete_draft(_, info, draft: int = 0):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if auth.user_id not in d.authors:
# raise BaseHttpException("only owner can remove coauthors")
return {
"error": "Only authors can update a draft"
}
elif not d:
return {
"error": "There is no draft with this id"
}
else:
session.delete(d)
session.commit()
return {}
@mutation.field("updateDraft") # TODO: draft input type
@login_required
async def update_draft(_, info, draft_input):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
d = session.query(
DraftCollab
).where(
DraftCollab.id == draft_input.id
).one() # raises Error when not found
if auth.user_id not in d.authors:
# raise BaseHttpException("only owner can remove coauthors")
return {
"error": "Only authors can update draft"
}
elif not d:
return {
"error": "There is no draft with this id"
}
else:
draft_input["updatedAt"] = datetime.now(tz=timezone.utc)
d.update(draft_input)
session.commit()
# TODO: email notify
return {}
@mutation.field("inviteAuthor")
@login_required
async def invite_coauthor(_, info, author: int = 0, draft: int = 0):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
c = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if auth.user_id not in c.authors:
# raise BaseHttpException("you are not in authors list")
return {
"error": "You are not in authors list"
}
elif c.id:
invited_user = session.query(User).where(User.id == author).one()
da = DraftAuthor.create({
"accepted": False,
"collab": c.id,
"author": invited_user.id
})
session.add(da)
session.commit()
else:
return {
"error": "Draft is not found"
}
# TODO: email notify
return {}
def get_slug(src):
slug = translit(src, "ru", reversed=True).replace(".", "-").lower()
slug = re.sub('[^0-9a-zA-Z]+', '-', slug)
return slug
@mutation.field("inviteAccept")
@login_required
async def accept_coauthor(_, info, draft: int):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if not d:
return {
"error": "Draft id was not found"
}
else:
a = session.query(DraftAuthor).where(DraftAuthor.collab == draft).filter(
DraftAuthor.author == auth.user_id).one()
if not a.accepted:
a.accepted = True
session.commit()
# TODO: email notify
return {}
elif a.accepted:
return {
"error": "You have accepted invite before"
}
else:
# raise BaseHttpException("only invited can accept")
return {
"error": "You don't have an invitation yet"
}
@mutation.field("draftToShout")
@login_required
async def draft_to_shout(_, info, draft: int = 0):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if auth.user_id not in d.authors:
# raise BaseHttpException("you are not in authors list")
return {
"error": "You are not in authors list"
}
elif d.id:
draft_authors = [a.author for a in d.authors]
draft_topics = [t.topic for t in d.topics]
authors = session.query(User).where(User.id._in(draft_authors)).all()
topics = session.query(Topic).where(Topic.id._in(draft_topics)).all()
new_shout = Shout.create({
"authors": authors,
"body": d.body,
"title": d.title,
"subtitle": d.subtitle or "",
"topics": topics,
"media": d.media,
"slug": d.slug or get_slug(d.title),
"layout": d.layout or "article"
})
session.add(new_shout)
session.commit()
else:
return {
"error": "Draft is not found"
}
# TODO: email notify
return {}

View File

@ -14,7 +14,6 @@ from resolvers.zine.reactions import reactions_follow, reactions_unfollow
from services.zine.gittask import GitTask
# from resolvers.inbox.chats import create_chat
# from services.inbox.storage import MessagesStorage
# from orm.draft import DraftCollab
@mutation.field("createShout")
@ -28,12 +27,12 @@ async def create_shout(_, info, inp):
new_shout = Shout.create(**{
"title": inp.get("title"),
"subtitle": inp.get('subtitle'),
"body": inp.get("body"),
"body": inp.get("body", ''),
"authors": inp.get("authors", []),
"slug": inp.get("slug"),
"mainTopic": inp.get("mainTopic"),
"visibility": "community",
# "createdBy": auth.user_id
"visibility": "owner",
"createdBy": auth.user_id
})
for topic in topics:
@ -82,21 +81,24 @@ async def create_shout(_, info, inp):
session.commit()
# TODO
# GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug)
# TODO
# GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug)
if new_shout.slug is None:
new_shout.slug = f"draft-{new_shout.id}"
session.commit()
return {"shout": new_shout}
@mutation.field("updateShout")
@login_required
async def update_shout(_, info, inp):
async def update_shout(_, info, slug, inp):
auth: AuthCredentials = info.context["request"].auth
slug = inp["slug"]
with local_session() as session:
user = session.query(User).filter(User.id == auth.user_id).first()
shout = session.query(Shout).filter(Shout.slug == slug).first()
if not shout:
return {"error": "shout not found"}
@ -109,18 +111,38 @@ async def update_shout(_, info, inp):
else:
shout.update(inp)
shout.updatedAt = datetime.now(tz=timezone.utc)
session.add(shout)
if inp.get("topics"):
# remove old links
links = session.query(ShoutTopic).where(ShoutTopic.shout == shout.id).all()
for topiclink in links:
session.delete(topiclink)
# add new topic links
for topic in inp.get("topics", []):
ShoutTopic.create(shout=slug, topic=topic)
# for topic_slug in inp.get("topics", []):
# topic = session.query(Topic).filter(Topic.slug == topic_slug).first()
# shout_topic = ShoutTopic.create(shout=shout.id, topic=topic.id)
# session.add(shout_topic)
session.commit()
# GitTask(inp, user.username, user.email, "update shout %s" % slug)
GitTask(inp, user.username, user.email, "update shout %s" % slug)
return {"shout": shout}
@mutation.field("publishShout")
@login_required
async def publish_shout(_, info, slug, inp):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
shout = session.query(Shout).filter(Shout.slug == slug).first()
if not shout:
return {"error": "shout not found"}
else:
shout.update(inp)
shout.visibility = "community"
shout.updatedAt = datetime.now(tz=timezone.utc)
session.commit()
return {"shout": shout}

View File

@ -1,11 +1,11 @@
from datetime import datetime, timedelta, timezone
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func, case
from sqlalchemy.sql.expression import desc, asc, select, func, case, and_
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.exceptions import ObjectNotExist
from base.exceptions import ObjectNotExist, OperationNotAllowed
from base.orm import local_session
from base.resolvers import query
from orm import ViewedEntry, TopicFollower
@ -70,7 +70,6 @@ def apply_filters(q, filters, user_id=None):
return q
@query.field("loadShout")
async def load_shout(_, info, slug):
with local_session() as session:
@ -196,6 +195,38 @@ async def load_shouts_by(_, info, options):
return shouts
@query.field("loadDrafts")
async def get_drafts(_, info, options):
auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id
q = select(Shout).options(
joinedload(Shout.authors),
joinedload(Shout.topics),
).where(
and_(Shout.deletedAt.is_(None), Shout.createdBy == user_id)
)
q = apply_filters(q, options.get("filters", {}), user_id)
order_by = options.get("order_by", Shout.createdAt)
if order_by == 'reacted':
aliased_reaction = aliased(Reaction)
q.outerjoin(aliased_reaction).add_columns(func.max(aliased_reaction.createdAt).label('reacted'))
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
offset = options.get("offset", 0)
limit = options.get("limit", 10)
q = q.group_by(Shout.id).order_by(query_order_by).limit(limit).offset(offset)
shouts = []
with local_session() as session:
for [shout] in session.execute(q).unique():
shouts.append(shout)
return shouts
@query.field("myFeed")
@login_required

View File

@ -60,7 +60,6 @@ type Author {
type Result {
error: String
uids: [String]
slugs: [String]
chat: Chat
chats: [Chat]
@ -77,8 +76,6 @@ type Result {
topics: [Topic]
community: Community
communities: [Community]
draft: DraftCollab
drafts: [DraftCollab]
}
enum ReactionStatus {
@ -100,7 +97,7 @@ type ReactionUpdating {
input ShoutInput {
slug: String
title: String
body: String!
body: String
authors: [String]
topics: [String]
community: Int
@ -130,16 +127,6 @@ input TopicInput {
# parents: [String]
}
input DraftInput {
slug: String
topics: [Int]
authors: [Int]
title: String
subtitle: String
body: String
cover: String
}
input ReactionInput {
kind: ReactionKind!
@ -183,8 +170,9 @@ type Mutation {
# shout
createShout(inp: ShoutInput!): Result!
updateShout(inp: ShoutInput!): Result!
updateShout(slug: String!, inp: ShoutInput!): Result!
deleteShout(slug: String!): Result!
publishShout(slug: String!, inp: ShoutInput!): Result!
# user profile
rateUser(slug: String!, value: Int!): Result!
@ -202,14 +190,6 @@ type Mutation {
updateReaction(id: Int!, reaction: ReactionInput!): Result!
deleteReaction(id: Int!): Result!
# draft / collab
createDraft(draft: DraftInput!): Result!
updateDraft(draft: DraftInput!): Result!
deleteDraft(draft: Int!): Result!
inviteAccept(draft: Int!): Result!
inviteAuthor(draft: Int!, author: Int!): Result!
draftToShout(draft: Int!): Result!
# following
follow(what: FollowingEntity!, slug: String!): Result!
unfollow(what: FollowingEntity!, slug: String!): Result!
@ -298,6 +278,7 @@ type Query {
loadAuthorsBy(by: AuthorsBy, limit: Int, offset: Int): [Author]!
loadShout(slug: String!): Shout
loadShouts(options: LoadShoutsOptions): [Shout]!
loadDrafts(options: LoadShoutsOptions): [Shout]!
loadReactionsBy(by: ReactionBy!, limit: Int, offset: Int): [Reaction]!
userFollowers(slug: String!): [Author]!
userFollowedAuthors(slug: String!): [Author]!
@ -306,9 +287,6 @@ type Query {
getAuthor(slug: String!): User
myFeed(options: LoadShoutsOptions): [Shout]
# draft/collab
loadDrafts: [DraftCollab]!
# migrate
markdownBody(body: String!): String!
@ -544,17 +522,3 @@ type Chat {
unread: Int
private: Boolean
}
type DraftCollab {
slug: String
title: String
subtitle: String
body: String
cover: String
layout: String
authors: [Int]!
topics: [String]
chat: Chat
createdAt: Int!
updatedAt: Int
}

View File

@ -169,9 +169,9 @@ class ViewedStorage:
viewed = session.query(
ViewedEntry
).join(
Shout
Shout, Shout.id == ViewedEntry.shout
).join(
User
User, User.id == ViewedEntry.viewer
).filter(
User.slug == viewer,
Shout.slug == shout_slug