Merge branch 'actionButtons' into 'master'

Action buttons

See merge request !34
This commit is contained in:
Lucas 2017-01-22 22:15:22 +00:00
commit 76ca9f1d14
8 changed files with 289 additions and 142 deletions

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/Sirupsen/logrus"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/odwrtw/papi" "github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib" polochon "github.com/odwrtw/polochon/lib"
@ -191,3 +192,38 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, movieList) return env.RenderJSON(w, movieList)
} }
// DeleteHandler deletes the movie from polochon
func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]
log := env.Log.WithFields(logrus.Fields{
"imdb_id": id,
"function": "movies.DeleteHandler",
})
log.Debugf("deleting movie")
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return fmt.Errorf("invalid user type")
}
var polochonConfig config.UserPolochon
err := user.GetConfig("polochon", &polochonConfig)
if err != nil {
return err
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return err
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
return client.Delete(&papi.Movie{ImdbID: id})
}

View File

@ -78,6 +78,7 @@ func main() {
env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}/get_details", movies.GetDetailsHandler).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/{id:tt[0-9]+}/get_details", movies.GetDetailsHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}", movies.DeleteHandler).WithRole(users.AdminRole).Methods("DELETE")
env.Handle("/movies/explore", extmedias.Explore).WithRole(users.UserRole).Methods("GET") env.Handle("/movies/explore", extmedias.Explore).WithRole(users.UserRole).Methods("GET")
env.Handle("/movies/refresh", extmedias.Refresh).WithRole(users.UserRole).Methods("POST") env.Handle("/movies/refresh", extmedias.Refresh).WithRole(users.UserRole).Methods("POST")
env.Handle("/movies/search", movies.SearchMovie).WithRole(users.UserRole).Methods("POST") env.Handle("/movies/search", movies.SearchMovie).WithRole(users.UserRole).Methods("POST")

View File

@ -61,7 +61,9 @@ export function updateUser(config) {
return request( return request(
'USER_UPDATE', 'USER_UPDATE',
configureAxios().post('/users/edit', config), configureAxios().post('/users/edit', config),
"User updated", [
addAlertOk("User updated"),
],
) )
} }
@ -90,6 +92,13 @@ export function selectMovie(imdbId) {
} }
} }
export function deleteMovieFromStore(imdbId) {
return {
type: 'DELETE_MOVIE',
imdbId
}
}
export function searchMovies(search) { export function searchMovies(search) {
return request( return request(
'SEARCH_MOVIES', 'SEARCH_MOVIES',
@ -104,6 +113,17 @@ export function getMovieDetails(imdbId) {
) )
} }
export function deleteMovie(imdbId) {
return request(
'MOVIE_DELETE',
configureAxios().delete(`/movies/${imdbId}`),
[
addAlertOk("Movie deleted"),
deleteMovieFromStore(imdbId),
],
)
}
export function fetchMovies(url) { export function fetchMovies(url) {
return request( return request(
'MOVIE_LIST_FETCH', 'MOVIE_LIST_FETCH',
@ -153,6 +173,8 @@ export function addTorrent(url) {
configureAxios().post('/torrents', { configureAxios().post('/torrents', {
url: url, url: url,
}), }),
"Torrent added", [
addAlertOk("Torrent added"),
],
) )
} }

View File

@ -0,0 +1,72 @@
import React from 'react'
import { DropdownButton, MenuItem } from 'react-bootstrap'
export default function ActionsButton(props) {
return (
<DropdownButton className="btn btn-default btn-sm" title="Actions" id="actions-button" dropup>
<RefreshButton
fetching={props.fetching}
movieId={props.movieId}
getDetails={props.getDetails}
/>
{(props.isUserAdmin && props.hasMovie) &&
<DeleteButton
movieId={props.movieId}
deleteMovie={props.deleteMovie}
isUserAdmin={props.isUserAdmin}
/>
}
</DropdownButton>
);
}
class RefreshButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
return
}
this.props.getDetails(this.props.movieId);
}
render() {
return (
<MenuItem onClick={this.handleClick}>
{this.props.fetching ||
<span>
<i className="fa fa-refresh"></i> Refresh
</span>
}
{this.props.fetching &&
<span>
<i className="fa fa-spin fa-refresh"></i> Refreshing
</span>
}
</MenuItem>
);
}
}
class DeleteButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
this.props.deleteMovie(this.props.movieId);
}
render() {
return (
<MenuItem onClick={this.handleClick}>
<span>
<i className="fa fa-trash"></i> Delete
</span>
</MenuItem>
);
}
}

View File

@ -1,58 +1,44 @@
import React from 'react' import React from 'react'
import TorrentsButton from './torrents' import TorrentsButton from './torrents'
import ActionsButton from './actions'
import ListPosters from '../list/posters' import ListPosters from '../list/posters'
import ListDetails from '../list/details' import ListDetails from '../list/details'
import Loader from '../loader/loader' import Loader from '../loader/loader'
class MovieButtons extends React.Component { function MovieButtons(props) {
constructor(props) { const imdb_link = `http://www.imdb.com/title/${props.movie.imdb_id}`;
super(props); const hasMovie = (props.movie.polochon_url !== "")
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
e.preventDefault();
if (this.props.fetching) {
return
}
this.props.getMovieDetails(this.props.movie.imdb_id);
}
render() {
const imdb_link = `http://www.imdb.com/title/${this.props.movie.imdb_id}`;
return ( return (
<div className="list-details-buttons btn-toolbar"> <div className="list-details-buttons btn-toolbar">
<a type="button" className="btn btn-default btn-sm" onClick={this.handleClick}> {hasMovie &&
{this.props.fetching || <a type="button" className="btn btn-primary btn-sm" href={props.movie.polochon_url}>
<span>
<i className="fa fa-refresh"></i> Refresh
</span>
}
{this.props.fetching &&
<span>
<i className="fa fa-spin fa-refresh"></i> Refreshing
</span>
}
</a>
{this.props.movie.polochon_url !== "" &&
<a type="button" className="btn btn-primary btn-sm" href={this.props.movie.polochon_url}>
<i className="fa fa-download"></i> Download <i className="fa fa-download"></i> Download
</a> </a>
} }
{this.props.movie.torrents && {props.movie.torrents &&
<TorrentsButton <TorrentsButton
torrents={this.props.movie.torrents} torrents={props.movie.torrents}
addTorrent={this.props.addTorrent} addTorrent={props.addTorrent}
/> />
} }
<ActionsButton
fetching={props.fetching}
movieId={props.movie.imdb_id}
getDetails={props.getMovieDetails}
deleteMovie={props.deleteMovie}
isUserAdmin={props.isUserAdmin}
hasMovie={hasMovie}
/>
<a type="button" className="btn btn-warning btn-sm" href={imdb_link}> <a type="button" className="btn btn-warning btn-sm" href={imdb_link}>
<i className="fa fa-external-link"></i> IMDB <i className="fa fa-external-link"></i> IMDB
</a> </a>
</div> </div>
); );
} }
}
export default class MovieList extends React.Component { export default class MovieList extends React.Component {
componentWillMount() { componentWillMount() {
@ -109,6 +95,8 @@ export default class MovieList extends React.Component {
fetching={this.props.movieStore.fetchingDetails} fetching={this.props.movieStore.fetchingDetails}
getMovieDetails={this.props.getMovieDetails} getMovieDetails={this.props.getMovieDetails}
addTorrent={this.props.addTorrent} addTorrent={this.props.addTorrent}
deleteMovie={this.props.deleteMovie}
isUserAdmin={this.props.userStore.isAdmin}
/> />
</ListDetails> </ListDetails>
} }

View File

@ -6,32 +6,7 @@ import { Nav, Navbar, NavItem, NavDropdown, MenuItem } from 'react-bootstrap'
import { LinkContainer } from 'react-router-bootstrap' import { LinkContainer } from 'react-router-bootstrap'
import { Control, Form } from 'react-redux-form'; import { Control, Form } from 'react-redux-form';
export default class NavBar extends React.Component { export default function NavBar(props) {
constructor(props) {
super(props);
this.handleMovieSearch = this.handleMovieSearch.bind(this);
this.handleShowSearch = this.handleShowSearch.bind(this);
}
handleMovieSearch() {
this.props.router.push(`/movies/search/${encodeURI(this.props.movieStore.search)}`)
}
handleShowSearch() {
this.props.router.push(`/shows/search/${encodeURI(this.props.showStore.search)}`)
}
render() {
const username = this.props.userStore.username;
const isLoggedIn = username !== "" ? true : false;
const location = this.props.router.getCurrentLocation().pathname;
let displayMovieSearch = false;
let displayShowSearch = false;
if (isLoggedIn && location.indexOf("movies") > -1)
{
displayMovieSearch = true;
}
if (isLoggedIn && location.indexOf("shows") > -1)
{
displayShowSearch = true;
}
return ( return (
<div> <div>
<Navbar fluid fixedTop collapseOnSelect> <Navbar fluid fixedTop collapseOnSelect>
@ -42,66 +17,117 @@ export default class NavBar extends React.Component {
<Navbar.Toggle /> <Navbar.Toggle />
</Navbar.Header> </Navbar.Header>
<Navbar.Collapse> <Navbar.Collapse>
<Nav> <MoviesDropdown />
<LinkContainer to="/movies/popular"> <ShowsDropdown />
<NavItem>Popular movies</NavItem> <UserDropdown
</LinkContainer> username={props.userStore.username}
<LinkContainer to="/movies/polochon"> logout={props.userLogout}
<NavItem>Polochon movies</NavItem> />
</LinkContainer> <Search
<LinkContainer to="/shows/popular"> model="movieStore"
<NavItem>Polochon shows</NavItem>
</LinkContainer>
</Nav>
<Nav pullRight>
{isLoggedIn ||
<LinkContainer to="/users/signup">
<NavItem>Sign up</NavItem>
</LinkContainer>
}
{isLoggedIn ||
<LinkContainer to="/users/login">
<NavItem>Login</NavItem>
</LinkContainer>
}
{isLoggedIn &&
<NavDropdown title={username} id="navbar-dropdown-right">
<LinkContainer to="/users/edit">
<MenuItem>Edit</MenuItem>
</LinkContainer>
<LinkContainer to="/users/logout" onClick={this.props.userLogout}>
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
}
</Nav>
{displayMovieSearch &&
<Navbar.Form pullRight>
<Form model="movieStore" className="input-group" onSubmit={() => this.handleMovieSearch()}>
<Control.text
model="movieStore.search" model="movieStore.search"
className="form-control"
placeholder="Search movies" placeholder="Search movies"
updateOn="change" router={props.router}
search={props.movieStore.search}
path='/movies/search'
pathMatch='movies'
/> />
</Form> <Search
</Navbar.Form> model="showStore"
}
{displayShowSearch &&
<Navbar.Form pullRight>
<Form model="showStore" className="input-group" onSubmit={() => this.handleShowSearch()}>
<Control.text
model="showStore.search" model="showStore.search"
className="form-control"
placeholder="Search shows" placeholder="Search shows"
updateOn="change" router={props.router}
search={props.showStore.search}
path='/shows/search'
pathMatch='shows'
/> />
</Form>
</Navbar.Form>
}
</Navbar.Collapse> </Navbar.Collapse>
</Navbar> </Navbar>
</div> </div>
); );
} }
class Search extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
}
handleSearch() {
this.props.router.push(`${this.props.path}/${encodeURI(this.props.search)}`)
}
render() {
const location = this.props.router.getCurrentLocation().pathname;
if (location.indexOf(this.props.pathMatch) === -1)
{
return null;
}
return(
<Navbar.Form pullRight>
<Form model={this.props.model} className="input-group" onSubmit={this.handleSearch}>
<Control.text
model={this.props.model}
className="form-control"
placeholder={this.props.placeholder}
updateOn="change"
/>
</Form>
</Navbar.Form>
);
}
}
function MoviesDropdown(props) {
return(
<Nav>
<NavDropdown title="Movies" id="navbar-movies-dropdown">
<LinkContainer to="/movies/popular">
<MenuItem>Popular</MenuItem>
</LinkContainer>
<LinkContainer to="/movies/polochon">
<MenuItem>Polochon</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
function ShowsDropdown(props) {
return(
<Nav>
<NavDropdown title="Shows" id="navbar-shows-dropdown">
<LinkContainer to="/shows/popular">
<NavItem>Popular</NavItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
}
function UserDropdown(props) {
if (props.username !== "") {
return (
<Nav pullRight>
<NavDropdown title={props.username} id="navbar-dropdown-right">
<LinkContainer to="/users/edit">
<MenuItem>Edit</MenuItem>
</LinkContainer>
<LinkContainer to="/users/logout" onClick={props.logout}>
<MenuItem>Logout</MenuItem>
</LinkContainer>
</NavDropdown>
</Nav>
);
} else {
return(
<Nav pullRight>
<LinkContainer to="/users/signup">
<NavItem>Sign up</NavItem>
</LinkContainer>
<LinkContainer to="/users/login">
<NavItem>Login</NavItem>
</LinkContainer>
</Nav>
);
}
} }

View File

@ -46,6 +46,11 @@ export default function movieStore(state = defaultState, action) {
movies: movies, movies: movies,
fetchingDetails: false, fetchingDetails: false,
}) })
case 'DELETE_MOVIE':
return Object.assign({}, state, {
movies: state.movies.filter((e) => (e.imdb_id !== action.imdbId)),
fetchingDetails: false,
})
case 'SELECT_MOVIE': case 'SELECT_MOVIE':
// Don't select the movie if we're fetching another movie's details // Don't select the movie if we're fetching another movie's details
if (state.fetchingDetails) { if (state.fetchingDetails) {

View File

@ -16,7 +16,7 @@ export function configureAxios(headers = {}) {
// This function takes en event prefix to dispatch evens during the life of the // This function takes en event prefix to dispatch evens during the life of the
// request, it also take a promise (axios request) // request, it also take a promise (axios request)
export function request(eventPrefix, promise, successMessage = null) { export function request(eventPrefix, promise, callbackEvents = null) {
// Events // Events
const pending = `${eventPrefix}_PENDING`; const pending = `${eventPrefix}_PENDING`;
const fulfilled = `${eventPrefix}_FULFILLED`; const fulfilled = `${eventPrefix}_FULFILLED`;
@ -40,13 +40,10 @@ export function request(eventPrefix, promise, successMessage = null) {
type: fulfilled, type: fulfilled,
payload: response.data, payload: response.data,
}) })
if (successMessage) { if (callbackEvents) {
dispatch({ for (let event of callbackEvents) {
type: 'ADD_ALERT_OK', dispatch(event);
payload: { }
message: successMessage,
},
})
} }
}) })
.catch(error => { .catch(error => {