Skip to content

Commit

Permalink
Merge pull request #358 from CPSSD/c/react-template
Browse files Browse the repository at this point in the history
Add ability for user to stick custom CSS into their posts
  • Loading branch information
CianLR authored Feb 18, 2019
2 parents 697b837 + d1cfe3b commit 78ca689
Show file tree
Hide file tree
Showing 19 changed files with 149 additions and 29 deletions.
42 changes: 42 additions & 0 deletions chump/src/components/account_edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface IAccountEditState {
currentPassword: string;
displayName: string;
newPassword: string;
postBodyCss: string;
postTitleCss: string;
privateAccount: boolean;
redirect: boolean;
}
Expand All @@ -26,6 +28,8 @@ export class AccountEdit extends React.Component<IAccountEditProps, IAccountEdit
currentPassword: "",
displayName: "",
newPassword: "",
postBodyCss: "",
postTitleCss: "",
privateAccount: false,
redirect: false,
};
Expand All @@ -34,6 +38,8 @@ export class AccountEdit extends React.Component<IAccountEditProps, IAccountEdit
this.handleNewPassword = this.handleNewPassword.bind(this);
this.handleBio = this.handleBio.bind(this);
this.handleDisplayName = this.handleDisplayName.bind(this);
this.handlePostTitleCss = this.handlePostTitleCss.bind(this);
this.handlePostBodyCss = this.handlePostBodyCss.bind(this);
this.handlePrivate = this.handlePrivate.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
this.handleCancel = this.handleCancel.bind(this);
Expand Down Expand Up @@ -67,6 +73,8 @@ export class AccountEdit extends React.Component<IAccountEditProps, IAccountEdit
this.state.currentPassword,
this.state.newPassword,
this.state.privateAccount,
this.state.postTitleCss,
this.state.postBodyCss,
).then((response: IEditUserResult) => {
if (!response.success) {
alert("Error editing: " + response.error);
Expand Down Expand Up @@ -131,6 +139,26 @@ export class AccountEdit extends React.Component<IAccountEditProps, IAccountEdit
onChange={this.handleBio}
/>
</div>
<div className="pure-control-group">
<label htmlFor="name">{"Post Title CSS"}</label>
<textarea
id="post_title_css"
placeholder='{"color": "red"}'
className="pure-input-2-3 bio-form"
value={this.state.postTitleCss}
onChange={this.handlePostTitleCss}
/>
</div>
<div className="pure-control-group">
<label htmlFor="name">{"Post Body CSS"}</label>
<textarea
id="post_body_css"
placeholder='{"color": "red"}'
className="pure-input-2-3 bio-form"
value={this.state.postBodyCss}
onChange={this.handlePostBodyCss}
/>
</div>

<div className="pure-control-group">
<label htmlFor="private">{config.private_account}</label>
Expand Down Expand Up @@ -221,6 +249,20 @@ export class AccountEdit extends React.Component<IAccountEditProps, IAccountEdit
});
}

private handlePostTitleCss(event: React.ChangeEvent<HTMLTextAreaElement>) {
const target = event.target;
this.setState({
postTitleCss: target.value,
});
}

private handlePostBodyCss(event: React.ChangeEvent<HTMLTextAreaElement>) {
const target = event.target;
this.setState({
postBodyCss: target.value,
});
}

private handleUpdateError(e: any) {
alert("Error attempting to update: " + e.toString());
}
Expand Down
2 changes: 1 addition & 1 deletion chump/src/components/create_article_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class CreateArticleForm extends React.Component<IFormProps, IFormState> {
</div>
<div className="pure-g" key={1}>
<HashRouter>
<Post username={this.props.username} blogPost={this.state.post} preview={true}/>
<Post username={this.props.username} blogPost={this.state.post} preview={true} customCss={true}/>
</HashRouter>
</div>
</RModal>
Expand Down
2 changes: 1 addition & 1 deletion chump/src/components/feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class Feed extends React.Component<IFeedProps, IFeedState> {
return this.state.publicBlog.map((e: IParsedPost, i: number) => {
return (
<div className="pure-g" key={i}>
<Post username={this.props.username} blogPost={e} preview={false}/>
<Post username={this.props.username} blogPost={e} preview={false} customCss={false}/>
</div>
);
});
Expand Down
11 changes: 10 additions & 1 deletion chump/src/components/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface IPostProps {
blogPost: IParsedPost;
username: string;
preview: boolean;
customCss: boolean;
}

interface IPostState {
Expand Down Expand Up @@ -47,6 +48,9 @@ export class Post extends React.Component<IPostProps, IPostState> {
this.props.preview === true) {
LikeButton = false;
}
// Set custom CSS for user if enabled.
const bodyStyle = this.props.customCss ? this.props.blogPost.body_css : undefined;
const titleStyle = this.props.customCss ? this.props.blogPost.title_css : undefined;
return (
<div className="blog-post-holder">
<div className="pure-u-5-24"/>
Expand All @@ -55,10 +59,15 @@ export class Post extends React.Component<IPostProps, IPostState> {
<Link
to={`/@${this.props.blogPost.author}/${this.props.blogPost.global_id}`}
className="article-title"
style={titleStyle}
>
{this.props.blogPost.title}
</Link>
<p className="article-body" dangerouslySetInnerHTML={{ __html: this.props.blogPost.body }}/>
<p
className="article-body"
style={bodyStyle}
dangerouslySetInnerHTML={{ __html: this.props.blogPost.body }}
/>
</div>
<div className="pure-u-1-24"/>
<div className="pure-u-3-24">
Expand Down
2 changes: 1 addition & 1 deletion chump/src/components/search_results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class SearchResults extends React.Component<ISearchResultsProps, ISearchR
return this.state.foundPosts.map((e: IParsedPost, i: number) => {
return (
<div className="pure-g pure-u-1" key={i}>
<Post username={this.props.username} blogPost={e} preview={false}/>
<Post username={this.props.username} blogPost={e} preview={false} customCss={false}/>
</div>
);
});
Expand Down
2 changes: 1 addition & 1 deletion chump/src/components/single_post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class SinglePost extends React.Component<ISinglePostProps, ISinglePostSta

return (
<div className="pure-g" key={1}>
<Post username={this.props.username} blogPost={this.state.posts[0]} preview={false}/>
<Post username={this.props.username} blogPost={this.state.posts[0]} preview={false} customCss={true}/>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion chump/src/components/user_feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class User extends React.Component<IUserProps, IUserState> {
return this.state.publicBlog.map((e: IParsedPost, i: number) => {
return (
<div className="pure-g" key={i}>
<Post username={this.props.username} blogPost={e} preview={false}/>
<Post username={this.props.username} blogPost={e} preview={false} customCss={true}/>
</div>
);
});
Expand Down
3 changes: 3 additions & 0 deletions chump/src/models/edit_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ export function EditUserPromise(
bio: string, displayName: string,
currentPassword: string, newPassword: string,
privateAccount: boolean,
postTitleCss: string, postBodyCss: string,
) {
const url = "/c2s/update/user";
const postBody = {
bio,
current_password: currentPassword,
display_name: displayName,
new_password: newPassword,
post_body_css: postBodyCss,
post_title_css: postTitleCss,
private: {
value: privateAccount,
},
Expand Down
45 changes: 36 additions & 9 deletions chump/src/models/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,66 @@ interface IBlogPost {
is_followed: boolean;
}

export interface IFeedResponse {
post_body_css: string;
post_title_css: string;
results: IBlogPost[];
}

export interface IParsedPost extends IBlogPost {
// parsed_date is a javascript Date object built from published key in
// IBlogPost
parsed_date: Date;
body_css?: React.CSSProperties;
title_css?: React.CSSProperties;
}

const feedApiURL = "/c2s/feed";
const perUserApiURL = "/c2s/@";

export function ParsePosts(b: IBlogPost[]) {
b = b as IParsedPost[];
function ParseCSSJson(j?: string) {
if (j === undefined || j === "") {
return undefined;
}
let p = {};
try {
p = JSON.parse(j);
} catch (err) {
// Invalid JSON.
return undefined;
}
// TODO(CianLR): Check if p is actually an instace of React.CSSProperties.
return p as React.CSSProperties;
}

export function ParsePosts(b: IBlogPost[], bodyCssJson?: string, titleCssJson?: string) {
const bodyCss = ParseCSSJson(bodyCssJson);
const titleCss = ParseCSSJson(titleCssJson);
// convert published string to js datetime obj
b = b as IParsedPost[];
b.map((e: IParsedPost) => {
e.parsed_date = new Date(e.published);
e.body_css = bodyCss;
e.title_css = titleCss;
if (e.bio === undefined || e.bio === "") {
e.bio = "Nowadays everybody wanna talk like they got something to say. \
But nothing comes out when they move their lips; just a bunch of gibberish.";
}
return e;
});
return b;
return b as IParsedPost[];
}

export function SortPosts(b: IBlogPost[]) {
b = ParsePosts(b);
b.sort((n: IParsedPost, m: IParsedPost) => {
export function SortPosts(b: IFeedResponse) {
const p: IParsedPost[] = ParsePosts(b.results, b.post_body_css, b.post_title_css);
p.sort((n: IParsedPost, m: IParsedPost) => {
return m.parsed_date.getTime() - n.parsed_date.getTime();
});
return b;
return p;
}

export function PostsAPIPromise(url: string) {
return new Promise<IBlogPost[]>((resolve, reject) => {
return new Promise<IParsedPost[]>((resolve, reject) => {
superagent
.get(url)
.set("Accept", "application/json")
Expand All @@ -58,7 +85,7 @@ export function PostsAPIPromise(url: string) {
// Feed will respond with an empty response if no blogs are avaiable.
let posts = res!.body;
if (posts === null) {
posts = [];
posts = {results: []};
}
const parsedPosts = SortPosts(posts);
resolve(parsedPosts);
Expand Down
31 changes: 25 additions & 6 deletions chump/test/models/posts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { expect } from "chai";
import * as sinon from "sinon";
import * as superagent from "superagent";

import { GetPublicPosts, GetSinglePost, IParsedPost } from "../../src/models/posts";
import { GetPublicPosts, GetSinglePost, IFeedResponse, IParsedPost } from "../../src/models/posts";

const sandbox: sinon.SinonSandbox = sinon.createSandbox();
const now: Date = new Date();

function createFakeResponse(body: IParsedPost[] | Error | null) {
function createFakeResponse(body: IFeedResponse | Error | null) {
const end = (cb: any) => {
cb(null, {ok: true, body });
};
Expand All @@ -18,10 +18,28 @@ function createFakeResponse(body: IParsedPost[] | Error | null) {
return sandbox.stub(superagent, "get").returns(root);
}

const validBody: IParsedPost[] = [{
const validBody: IFeedResponse = {
post_body_css: "",
post_title_css: "",
results: [{
author: "aaron",
bio: "bio",
body: "rm -rf steely/",
global_id: 2,
image: "",
is_followed: false,
is_liked: false,
likes_count: 1,
published: "",
title: "how to write a plugin",
}],
};

const evalidBody: IParsedPost[] = [{
author: "aaron",
bio: "bio",
body: "rm -rf steely/",
body_css: undefined,
global_id: 2,
image: "",
is_followed: false,
Expand All @@ -30,6 +48,7 @@ const validBody: IParsedPost[] = [{
parsed_date: now,
published: "",
title: "how to write a plugin",
title_css: undefined,
}];

describe("GetPublicPosts", () => {
Expand All @@ -43,7 +62,7 @@ describe("GetPublicPosts", () => {
GetPublicPosts().then((posts: IParsedPost[]) => {
expect(getRequest).to.have.property("callCount", 1);
expect(getRequest.calledWith("/c2s/feed")).to.be.ok;
expect(posts).to.eql(validBody);
expect(posts).to.eql(validBody.results);
done();
});
});
Expand All @@ -53,7 +72,7 @@ describe("GetPublicPosts", () => {
GetPublicPosts("username").then((posts: IParsedPost[]) => {
expect(getRequest).to.have.property("callCount", 1);
expect(getRequest.calledWith("/c2s/feed/username")).to.be.ok;
expect(posts).to.eql(validBody);
expect(posts).to.eql(validBody.results);
done();
});
});
Expand Down Expand Up @@ -85,7 +104,7 @@ describe("GetPublicPosts", () => {
GetSinglePost("username", "id").then((posts: IParsedPost[]) => {
expect(getRequest).to.have.property("callCount", 1);
expect(getRequest.calledWith("/c2s/@username/id")).to.be.ok;
expect(posts).to.eql(validBody);
expect(posts).to.eql(validBody.results);
done();
});
});
Expand Down
2 changes: 2 additions & 0 deletions services/database/rabble_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ CREATE TABLE IF NOT EXISTS users (
/* account_type refers to the account_type enum in the database.proto file.
* In normal cases, this will be 0, a normal user. */
private boolean NOT NULL,
post_title_css text NOT NULL DEFAULt '',
post_body_css text NOT NULL DEFAULT '',
UNIQUE (handle, host)
);

Expand Down
7 changes: 5 additions & 2 deletions services/database/users_servicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def __init__(self, db, logger):
self._select_base = (
"SELECT u.global_id, u.handle, u.host, u.display_name, "
"u.password, u.bio, u.rss, u.private, "
"f.follower IS NOT NULL FROM users u "
"f.follower IS NOT NULL, "
"u.post_title_css, u.post_body_css FROM users u "
"LEFT OUTER JOIN follows f ON "
"f.followed=u.global_id AND f.follower=? "
)
Expand All @@ -38,7 +39,7 @@ def _private_to_filter(self, entry):
return entry.private.value

def _db_tuple_to_entry(self, tup, entry):
if len(tup) != 9:
if len(tup) != 11:
self._logger.warning(
"Error converting tuple to UsersEntry: " +
"Wrong number of elements " + str(tup))
Expand All @@ -54,6 +55,8 @@ def _db_tuple_to_entry(self, tup, entry):
entry.rss = tup[6]
entry.private.value = tup[7]
entry.is_followed = tup[8]
entry.post_title_css = tup[9]
entry.post_body_css = tup[10]
except Exception as e:
self._logger.warning(
"Error converting tuple to UsersEntry: " +
Expand Down
Loading

0 comments on commit 78ca689

Please sign in to comment.