From 4e3026daca566128e9f2b92245180f8ef2058995 Mon Sep 17 00:00:00 2001 From: bniwredyc Date: Tue, 28 Mar 2023 23:57:40 +0200 Subject: [PATCH 1/2] drafts removed, createdBy and deletedBy added to shout --- orm/draft.py | 41 -------- orm/shout.py | 6 +- resolvers/__init__.py | 2 - resolvers/create/drafts.py | 189 ------------------------------------- resolvers/create/editor.py | 3 +- schema.graphql | 37 -------- 6 files changed, 5 insertions(+), 273 deletions(-) delete mode 100644 orm/draft.py delete mode 100644 resolvers/create/drafts.py diff --git a/orm/draft.py b/orm/draft.py deleted file mode 100644 index bae7f1c0..00000000 --- a/orm/draft.py +++ /dev/null @@ -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) diff --git a/orm/shout.py b/orm/shout.py index c9044a83..5f3e2f4e 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -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) diff --git a/resolvers/__init__.py b/resolvers/__init__.py index df652076..ab919515 100644 --- a/resolvers/__init__.py +++ b/resolvers/__init__.py @@ -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 diff --git a/resolvers/create/drafts.py b/resolvers/create/drafts.py deleted file mode 100644 index de33481e..00000000 --- a/resolvers/create/drafts.py +++ /dev/null @@ -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 {} diff --git a/resolvers/create/editor.py b/resolvers/create/editor.py index b03a3933..36d60668 100644 --- a/resolvers/create/editor.py +++ b/resolvers/create/editor.py @@ -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") @@ -33,7 +32,7 @@ async def create_shout(_, info, inp): "slug": inp.get("slug"), "mainTopic": inp.get("mainTopic"), "visibility": "community", - # "createdBy": auth.user_id + "createdBy": auth.user_id }) for topic in topics: diff --git a/schema.graphql b/schema.graphql index 74036c6f..f45ee883 100644 --- a/schema.graphql +++ b/schema.graphql @@ -77,8 +77,6 @@ type Result { topics: [Topic] community: Community communities: [Community] - draft: DraftCollab - drafts: [DraftCollab] } enum ReactionStatus { @@ -130,16 +128,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! @@ -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! @@ -306,9 +286,6 @@ type Query { getAuthor(slug: String!): User myFeed(options: LoadShoutsOptions): [Shout] - # draft/collab - loadDrafts: [DraftCollab]! - # migrate markdownBody(body: String!): String! @@ -544,17 +521,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 -} From 3836674b726f22aa6703b16b830bfbccbc5047fb Mon Sep 17 00:00:00 2001 From: bniwredyc Date: Wed, 3 May 2023 17:47:09 +0200 Subject: [PATCH 2/2] new create shout flow --- migrate.sh | 19 ++++++++++++++++ resolvers/create/editor.py | 45 ++++++++++++++++++++++++++++---------- resolvers/zine/load.py | 37 ++++++++++++++++++++++++++++--- schema.graphql | 7 +++--- services/stat/viewed.py | 4 ++-- 5 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 migrate.sh diff --git a/migrate.sh b/migrate.sh new file mode 100644 index 00000000..2c1189da --- /dev/null +++ b/migrate.sh @@ -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!' + diff --git a/resolvers/create/editor.py b/resolvers/create/editor.py index 36d60668..a6412436 100644 --- a/resolvers/create/editor.py +++ b/resolvers/create/editor.py @@ -27,11 +27,11 @@ 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", + "visibility": "owner", "createdBy": auth.user_id }) @@ -81,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"} @@ -108,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} diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 6c42fad0..90315b63 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -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 diff --git a/schema.graphql b/schema.graphql index f45ee883..7c75a79d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -60,7 +60,6 @@ type Author { type Result { error: String - uids: [String] slugs: [String] chat: Chat chats: [Chat] @@ -98,7 +97,7 @@ type ReactionUpdating { input ShoutInput { slug: String title: String - body: String! + body: String authors: [String] topics: [String] community: Int @@ -171,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! @@ -278,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]! diff --git a/services/stat/viewed.py b/services/stat/viewed.py index e1ef0a58..779a6e93 100644 --- a/services/stat/viewed.py +++ b/services/stat/viewed.py @@ -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