Skip to content

albperleo/IISSI2

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
DeliverUS-Frontend-Customer
 
 
entregable-backend-l6-md-2
 
 
 
 

RESUMEN EXAMEN DE LABORATORIO IISSI 2

1. Inicialización

  1. Importar repositorios a VSCode con git clone <URL>.
  2. Instalar dependencias de back y front npm install.
  3. Realizamos las migraciones a la DB:
    1. Abrimos HeidiSQL y creamos DB nueva con el usuario "iissi_user" y contraseña "iissi$user". Comprobamos puertos de aplicación con el .env.
    2. Ejecutamos migraciones y seeders. Ctrl+Shift+P o F1 y Tasks: run task y seleccionamos Rebuild database.
  4. Ejecutamos en backend npm start.
  5. LEEMOS ENUNCIADO!!

2. Backend

Procedimiento

  1. Cambiar models y migrations conforme enunciado.
  2. Añadir nuevo apartado en Validation.
    1. HAY QUE ACTUALIZAR TANTO EN CREATE COMO EN UPDATE!!
  3. En su caso, añadir nuevo apartado en Controllers.

Laboratorios de backend

  1. LAB 2 - Routing & Controllers: https://github.com/IISSI2-IS/Lab2-Backend-Routing-Controllers
  2. LAB 3 - Validation & Middleware: https://github.com/IISSI2-IS/Lab3-Backend-Validation-Middleware

Ejemplo de Route

app.route('/products')
    .post(
      middlewares.isLoggedIn,
      middlewares.hasRole('owner'),
      upload,
      middlewares.checkProductRestaurantOwnership,
      ProductValidation.create(),
      ProductController.create
    )

Examen Junio - Tarde:

Todos los cambios dentro de MODELS en archivo product.js:

Product.init({
    name: DataTypes.STRING,
    description: DataTypes.STRING,
    fats: DataTypes.DOUBLE, //CAMBIO
    proteins: DataTypes.DOUBLE, //CAMBIO
    carbohydrates: DataTypes.DOUBLE, //CAMBIO
    calories: DataTypes.DOUBLE, //CAMBIO
    price: DataTypes.DOUBLE,
    image: DataTypes.STRING,
    order: DataTypes.INTEGER,
    availability: DataTypes.BOOLEAN,
    restaurantId: DataTypes.INTEGER,
    productCategoryId: DataTypes.INTEGER
  }, {
    sequelize,
    modelName: 'Product'
  })
  return Product
}

Todos los cambios dentro de MIGRATIONS en archivo create-product.js:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Products', {
      ...
      fats: {
        type: Sequelize.DOUBLE,
        allowNull: false
      },
      proteins: {
        type: Sequelize.DOUBLE,
        allowNull: false
      },
      carbohydrates: {
        type: Sequelize.DOUBLE,
        allowNull: false
      },
      calories: {
        type: Sequelize.DOUBLE,
        allowNull: false
      },
      ...
    })
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Products')
  }
}

Todos los cambios dentro de la VALIDATIONS en archivo ProductValidation.js:

const calculateCalories = (fats, proteins, carbohydrates) => {
  fats = parseFloat(fats)
  proteins = parseFloat(proteins)
  carbohydrates = parseFloat(carbohydrates)

  const calories = 9 * fats + 4 * proteins + 4 * carbohydrates

  if (calories > 1000 || fats + proteins + carbohydrates !== 100) {
    return false
  } else {
    return true
  }
}

module.exports = {
    create: () => {
        return [
            ...
            check('fats')
            .custom((value, { req }) => {
                return calculateCalories(value, req.body.proteins, req.body.carbohydrates)
            })
            .withMessage('Un alimento no puede superar las 1000 calorias por cada 100 gramos.')
        ]
    },
    update: () => {
        return [
            ...
            check('fats')
            .custom((value, { req }) => {
                return calculateCalories(value, req.body.proteins, req.body.carbohydrates)
            })
            .withMessage('Un alimento no puede superar las 1000 calorias por cada 100 gramos.')
        ]
    }
}
        

En CONTROLLERS no se realiza ningún cambio.

Examen Junio - Mañana:

Todos los cambios dentro de MODELS en archivo restaurant.js:

Restaurant.init({
    name: DataTypes.STRING,
    promoted: DataTypes.BOOLEAN, //CAMBIO
    description: DataTypes.TEXT,
    ...
  }, {
    sequelize,
    modelName: 'Restaurant'
  })
  return Restaurant
}

Todos los cambios dentro de MIGRATIONS en archivo create-restaurant.js:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Restaurants', {
      ...
      promoted: { //CAMBIO
        allowNull: false,
        type: Sequelize.BOOLEAN,
        defaultValue: true
      },
      ...
    })
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Restaurants')
  }
}

Todos los cambios dentro de VALIDATIONS en archivo RestaurantValidation.js:

const promotedRestaurant = async (ownerId, isPromoted) => {
  let ruleBroken = false
  if (isPromoted === true) {
    try {
      const ownerRestaurants = await Restaurant.findAll({
        where: {
          userId: ownerId,
          promoted: true
        }
      })
      if (isPromoted && ownerRestaurants.lenght > 0) {
        ruleBroken = true
      }
    } catch (error) {
      ruleBroken = true
    }
  }
  return ruleBroken ? Promise.reject(new Error('You can only promote one restaurant at a time')) : Promise.resolve()
}

module.exports = {
    create: () => {
        return [
            ...
            check('promoted')
            .custom((value, { req }) => {
                return promotedRestaurant(req.user.id, value)
            })
            .withMessage('You cant promote more than one restaurant at the same time.')
        ]
    },
    update: () => {
        return [
            ...
            check('promoted')
            .custom((value, { req }) => {
                return promotedRestaurant(req.user.id, value)
            })
            .withMessage('You cant promote more than one restaurant at the same time.')
        ]
    }
}

Todos los cambios dentro de CONTROLLERS en archivo RestaurantController.js:

exports.show = async function (req, res) {
  // Only returns PUBLIC information of restaurants
  try {
    const restaurant = await Restaurant.findByPk(req.params.restaurantId, {
      attributes: { exclude: ['userId'] },
      include: [{
        model: Product,
        as: 'products',
        order: [['promoted', 'DESC']], //ESTOS SON LOS CAMBIOS
        include: { model: ProductCategory, as: 'productCategory' }
      },
      {
        model: RestaurantCategory,
        as: 'restaurantCategory'
      }]
    }
    )
    res.json(restaurant)
  } catch (err) {
    res.status(404).send(err)
  }
}

3. Frontend

Procedimiento

Laboratorios de Frontend

  1. LAB 4 - Setup & Navigation: https://github.com/IISSI2-IS/Lab4-FrontEnd-Setup-Navigation
  2. LAB 5 - ReactNative Basics: https://github.com/IISSI2-IS/Lab5-FrontEnd-ReactNativeBasics
  3. LAB 6 - RestfulAPI & Queries: https://github.com/IISSI2-IS/Lab6-FrontEnd-RestfulAPI-Queries
  4. LAB 7 - FlexLayout & Forms: https://github.com/IISSI2-IS/Lab7-FrontEnd-FlexLayout-Forms
  5. LAB 8 - Forms Validation & POST Requests: https://github.com/IISSI2-IS/Lab8-FormsValidation-POSTRequests

Ejemplo de EndPoints RestaurantEndPoints.js

import { get, post } from './helpers/ApiRequestsHelper'
function getAll () {
  return get('users/myrestaurants')
}

function getDetail (id) {
  return get(`restaurants/${id}`)
}

function getRestaurantCategories () {
  return get('restaurantCategories')
}

function create (data) {
  return post('restaurants', data)
}

export { getAll, getDetail, getRestaurantCategories, create }

Examen Junio - Tarde:

Todos los cambios dentro de SCREENS en archivo CreateProductScreen.js:

export default function CreateProductScreen ({ navigation, route }) {
  const [open, setOpen] = useState(false)
  const [productCategories, setProductCategories] = useState([])
  const [backendErrors, setBackendErrors] = useState()

  const initialProductValues = { name: '', description: '', price: 0, order: 0, fats: 0, proteins: 0, carbohydrates: 0, restaurantId: route.params.id, productCategoryId: null, availability: true }
  const validationSchema = yup.object().shape({
    name: yup
      .string()
      .max(30, 'Name too long')
      .required('Name is required'),
    price: yup
      .number()
      .positive('Please provide a positive price value')
      .required('Price is required'),
    order: yup
      .number()
      .positive('Please provide a positive cost value')
      .integer('Please provide an integer cost value'),
    // Solution
    fats: yup
      .number()
      .positive('Please provide a positive cost value')
      .max(100, 'The values must be lower or equal than 100'),
    proteins: yup
      .number()
      .positive('Please provide a positive cost value')
      .max(100, 'The values must be lower or equal than 100'),
    carbohydrates: yup
      .number()
      .positive('Please provide a positive cost value')
      .max(100, 'The values must be lower or equal than 100')
  })

  useEffect(() => {
    ...
    fetchProductCategories()
  }, [])

  const pickImage = async (onSuccess) => {
    ...
  }

  const createProduct = async (values) => {
    setBackendErrors([])
    try {
      console.log(values)
      const createdProduct = await create(values)
      showMessage({
        message: `Product ${createdProduct.name} succesfully created`,
        type: 'success',
        style: flashStyle,
        titleStyle: flashTextStyle
      })
      navigation.navigate('RestaurantDetailScreen', { id: route.params.id, dirty: true })
    } catch (error) {
      console.log(error)
      setBackendErrors(error.errors)
    }
  }
  return (
    <Formik
      validationSchema={validationSchema}
      initialValues={initialProductValues}
      onSubmit={createProduct}>
      {({ handleSubmit, setFieldValue, values }) => (
        <ScrollView>
          <View style={{ alignItems: 'center' }}>
            <View style={{ width: '60%' }}>
              <InputItem
                name='name'
                label='Name:'
              />
              <InputItem
                name='description'
                label='Description:'
              />
              <InputItem
                name='price'
                label='Price:'
              />
              <InputItem
                name='order'
                label='Order/position to be rendered:'
              />
              {/* Solution */}
              <InputItem
                name='fats'
                label='Fats:'
              />
              <InputItem
                name='proteins'
                label='Proteins:'
              />
              <InputItem
                name='carbohydrates'
                label='Carbohydrates:'
              />
              <DropDownPicker
                open={open}
                value={values.productCategoryId}
                items={productCategories}
                setOpen={setOpen}
                onSelectItem={item => {
                  setFieldValue('productCategoryId', item.value)
                }}
                setItems={setProductCategories}
                placeholder="Select the product category"
                containerStyle={{ height: 40, marginTop: 20, marginBottom: 20 }}
                style={{ backgroundColor: brandBackground }}
                dropDownStyle={{ backgroundColor: '#fafafa' }}
              />

              <TextRegular>Is it available?</TextRegular>
              <Switch
                trackColor={{ false: brandSecondary, true: brandPrimary }}
                thumbColor={values.availability ? brandSecondary : '#f4f3f4'}
                // onValueChange={toggleSwitch}
                value={values.availability}
                style={styles.switch}
                onValueChange={value =>
                  setFieldValue('availability', value)
                }
              />

              <Pressable onPress={() =>
                pickImage(
                  async result => {
                    await setFieldValue('image', result)
                  }
                )
              }
                style={styles.imagePicker}
              >
                <TextRegular>Product image: </TextRegular>
                <Image style={styles.image} source={values.image ? { uri: values.image.uri } : defaultProduct} />
              </Pressable>

              {backendErrors &&
                backendErrors.map((error, index) => <TextError key={index}>{error.msg}</TextError>)
              }

              <Pressable
                onPress={ handleSubmit }
                style={({ pressed }) => [
                  {
                    backgroundColor: pressed
                      ? brandPrimaryTap
                      : brandPrimary
                  },
                  styles.button
                ]}>
                <TextRegular textStyle={styles.text}>
                  Create product
                </TextRegular>
              </Pressable>
            </View>
          </View>
        </ScrollView>
      )}
    </Formik>
  )
}

const styles = StyleSheet.create({
  ...
})

Todos los cambios dentro de SCREENS en archivo RestaurantDetailScreen.js:

export default function RestaurantDetailScreen ({ navigation, route }) {
  const [restaurant, setRestaurant] = useState({})

  useEffect(() => {
    ...
  }, [route])

  const renderHeader = () => {
    return (
      ...
    )
  }

  const renderProduct = ({ item }) => {
    return (
      <ImageCard
        imageUri={item.image ? { uri: process.env.API_BASE_URL + '/' + item.image } : undefined}
        title={item.name}
      >
        <TextRegular numberOfLines={2}>{item.description}</TextRegular>
        <TextSemiBold textStyle={styles.price}>{item.price.toFixed(2)}</TextSemiBold>
        {/* Solution */}
        {item.fats && <TextSemiBold>Nutritional composition:</TextSemiBold>}
        {item.fats && <View style={{ flexDirection: 'row', paddingLeft: 10 }}><TextSemiBold>Fats: </TextSemiBold> <TextRegular>{item.fats.toFixed(2)}</TextRegular></View>}
        {item.proteins && <View style={{ flexDirection: 'row', paddingLeft: 10 }}><TextSemiBold>Proteins: </TextSemiBold> <TextRegular>{item.proteins.toFixed(2)}</TextRegular></View>}
        {item.carbohydrates && <View style={{ flexDirection: 'row', paddingLeft: 10 }}><TextSemiBold>Carbohydrates: </TextSemiBold> <TextRegular>{item.carbohydrates.toFixed(2)}</TextRegular></View>}
        {item.calories && <View style={{ flexDirection: 'row', paddingLeft: 10 }}><TextSemiBold>Calories: </TextSemiBold> <TextRegular>{item.calories.toFixed(2)}</TextRegular></View>}

      </ImageCard>
    )
  }

  const renderEmptyProductsList = () => {
    ...
  }

  return (
    <View style={styles.container}>
      <FlatList
        ListHeaderComponent={renderHeader}
        ListEmptyComponent={renderEmptyProductsList}
        style={styles.container}
        data={restaurant.products}
        renderItem={renderProduct}
        keyExtractor={item => item.id.toString()}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  ...
})

Examen Junio - Mañana:

Todos los cambios dentro de SCREENS en archivo CreateRestaurantScreen.js:

export default function CreateRestaurantScreen ({ navigation }) {
  const [open, setOpen] = useState(false)
  const [restaurantCategories, setRestaurantCategories] = useState([])
  const [backendErrors, setBackendErrors] = useState()

  // SOLUTION
  const initialRestaurantValues = { name: '', description: '', address: '', postalCode: '', url: '', shippingCosts: 0, email: '', phone: '', restaurantCategoryId: '', promoted: true }

  const validationSchema = yup.object().shape({
    ...
  })

  useEffect(() => {
    ...
  }, [])

  React.useEffect(() => {
    ...
  }, [])

  const pickImage = async (onSuccess) => {
    ...
  }

  const createRestaurant = async (values) => {
    ...
  }

  return (
    <Formik
      validationSchema={validationSchema}
      initialValues={initialRestaurantValues}
      onSubmit={createRestaurant}>
      {({ handleSubmit, setFieldValue, values }) => (
        <ScrollView>
          <View style={{ alignItems: 'center' }}>
            <View style={{ width: '60%' }}>
              <InputItem
                name='name'
                label='Name:'
              />
              <InputItem
                name='description'
                label='Description:'
              />
              <InputItem
                name='address'
                label='Address:'
              />
              <InputItem
                name='postalCode'
                label='Postal code:'
              />
              <InputItem
                name='url'
                label='Url:'
              />
              <InputItem
                name='shippingCosts'
                label='Shipping costs:'
              />
              <InputItem
                name='email'
                label='Email:'
              />
              <InputItem
                name='phone'
                label='Phone:'
              />

              <DropDownPicker
                open={open}
                value={values.restaurantCategoryId}
                items={restaurantCategories}
                setOpen={setOpen}
                onSelectItem={ item => {
                  setFieldValue('restaurantCategoryId', item.value)
                }}
                setItems={setRestaurantCategories}
                placeholder="Select the restaurant category"
                containerStyle={{ height: 40, marginTop: 20 }}
                style={{ backgroundColor: brandBackground }}
                dropDownStyle={{ backgroundColor: '#fafafa' }}
              />
              <ErrorMessage name={'restaurantCategoryId'} render={msg => <TextError>{msg}</TextError> }/>

              <Pressable onPress={() =>
                pickImage(
                  async result => {
                    await setFieldValue('logo', result)
                  }
                )
              }
                style={styles.imagePicker}
              >
                <TextRegular>Logo: </TextRegular>
                <Image style={styles.image} source={values.logo ? { uri: values.logo.uri } : restaurantLogo} />
              </Pressable>

              <Pressable onPress={() =>
                pickImage(
                  async result => {
                    await setFieldValue('heroImage', result)
                  }
                )
              }
                style={styles.imagePicker}
              >
                <TextRegular>Hero image: </TextRegular>
                <Image style={styles.image} source={values.heroImage ? { uri: values.heroImage.uri } : restaurantBackground} />
              </Pressable>

              {/* SOLUTION */}
              <TextRegular>Is it promoted?</TextRegular>
              <Switch
                trackColor={{ false: brandSecondary, true: brandPrimary }}
                thumbColor={values.promoted ? brandSecondary : '#f4f3f4'}
                value={values.promoted}
                style={styles.switch}
                onValueChange={value =>
                  setFieldValue('promoted', value)
                }
              />

              {backendErrors &&
                backendErrors.map((error, index) => <TextError key={index}>{error.msg}</TextError>)
              }

              <Pressable
                onPress={handleSubmit}
                style={({ pressed }) => [
                  {
                    backgroundColor: pressed
                      ? brandPrimaryTap
                      : brandPrimary
                  },
                  styles.button
                ]}>
                <TextRegular textStyle={styles.text}>
                  Create restaurant
                </TextRegular>
              </Pressable>
            </View>
          </View>
        </ScrollView>
      )}
    </Formik>
  )
}

const styles = StyleSheet.create({
  ...
  // SOLUTION
  switch: {
    marginTop: 5
  }
})

Todos los cambios dentro de SCREENS en archivo RestaurantsScreen.js:

export default function RestaurantsScreen ({ navigation, route }) {
  const [restaurants, setRestaurants] = useState([])
  const { loggedInUser } = useContext(AuthorizationContext)

  useEffect(() => {
    ...
  }, [loggedInUser, route])

  const renderRestaurant = ({ item }) => {
    return (
      <ImageCard
        imageUri={item.logo ? { uri: process.env.API_BASE_URL + '/' + item.logo } : undefined}
        title={item.name}
        onPress={() => {
          navigation.navigate('RestaurantDetailScreen', { id: item.id })
        }}
      >
         {/* SOLUTION */}
         {item.promoted &&
          <TextRegular textStyle={{ color: brandPrimary, textAlign: 'right' }}>En promoción!</TextRegular>
        }
        <TextRegular numberOfLines={2}>{item.description}</TextRegular>
        {item.averageServiceMinutes !== null &&
          <TextSemiBold>Avg. service time: <TextSemiBold textStyle={{ color: brandPrimary }}>{item.averageServiceMinutes} min.</TextSemiBold></TextSemiBold>
        }
        <TextSemiBold>Shipping: <TextSemiBold textStyle={{ color: brandPrimary }}>{item.shippingCosts.toFixed(2)}</TextSemiBold></TextSemiBold>
      </ImageCard>
    )
  }

  const renderEmptyRestaurantsList = () => {
    ...
  }

  const renderHeader = () => {
    ...
  }

  return (
    <FlatList
      style={styles.container}
      data={restaurants}
      renderItem={renderRestaurant}
      keyExtractor={item => item.id.toString()}
      ListHeaderComponent={renderHeader}
      ListEmptyComponent={renderEmptyRestaurantsList}
    />
  )
}

const styles = StyleSheet.create({
  ...
})

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published