eintopf.info@v0.13.16/service/place/place.go (about)

     1  // Copyright (C) 2022 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package place
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"eintopf.info/internal/crud"
    24  	"eintopf.info/internal/xerror"
    25  	"eintopf.info/service/auth"
    26  )
    27  
    28  // Place defines a place entity
    29  type NewPlace struct {
    30  	Published   bool     `json:"published"`
    31  	Name        string   `json:"name"`
    32  	Address     string   `json:"address"`
    33  	Lat         float64  `json:"lat"`
    34  	Lng         float64  `json:"lng"`
    35  	Link        string   `json:"link"`
    36  	Email       string   `json:"email"`
    37  	Description string   `json:"description"`
    38  	Image       string   `json:"image"`
    39  	OwnedBy     []string `json:"ownedBy" db:"owned_by"`
    40  }
    41  
    42  // IsOwned returns true if the id is in the OwnedBy field.
    43  func (p *NewPlace) IsOwned(id string) bool {
    44  	owned := false
    45  	for _, owner := range p.OwnedBy {
    46  		if owner == id {
    47  			owned = true
    48  		}
    49  	}
    50  	return owned
    51  }
    52  
    53  // Place defines a place entity
    54  // It implements indexo.Coppyable.
    55  type Place struct {
    56  	ID          string   `json:"id" db:"id"`
    57  	Deactivated bool     `json:"deactivated" db:"deactivated"`
    58  	Published   bool     `json:"published" db:"published"`
    59  	Name        string   `json:"name" db:"name"`
    60  	Description string   `json:"description" db:"description"`
    61  	Email       string   `json:"email" db:"email"`
    62  	Image       string   `json:"image" db:"image"`
    63  	Link        string   `json:"link" db:"link"`
    64  	Address     string   `json:"address" db:"address"`
    65  	Lat         float64  `json:"lat" db:"lat"`
    66  	Lng         float64  `json:"lng" db:"lng"`
    67  	OwnedBy     []string `json:"ownedBy" db:"owned_by"`
    68  }
    69  
    70  func (p Place) Identifier() string { return p.ID }
    71  
    72  func PlaceFromNewPlace(newPlace *NewPlace, id string) *Place {
    73  	return &Place{
    74  		ID:          id,
    75  		Deactivated: false,
    76  		Name:        newPlace.Name,
    77  		Address:     newPlace.Address,
    78  		Lat:         newPlace.Lat,
    79  		Lng:         newPlace.Lng,
    80  		Link:        newPlace.Link,
    81  		Email:       newPlace.Email,
    82  		Description: newPlace.Description,
    83  		Published:   newPlace.Published,
    84  		Image:       newPlace.Image,
    85  		OwnedBy:     newPlace.OwnedBy,
    86  	}
    87  }
    88  
    89  // Indexable indicates wether a place should be added to the search index.
    90  func (p *Place) Indexable() bool {
    91  	return !p.Deactivated
    92  }
    93  
    94  // Listable indicates wether a place should be shown on the list page.
    95  func (p *Place) Listable() bool {
    96  	return p.Published
    97  }
    98  
    99  // IsOwned returns true if the id is in the OwnedBy field.
   100  func (p *Place) IsOwned(id string) bool {
   101  	owned := false
   102  	for _, owner := range p.OwnedBy {
   103  		if owner == id {
   104  			owned = true
   105  		}
   106  	}
   107  	return owned
   108  }
   109  
   110  func (p *Place) Coordinates() (float64, float64) {
   111  	return p.Lat, p.Lng
   112  }
   113  
   114  // Service defines the crud service to manage places.
   115  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/place Service --output=../../internal/mock/place_service.go --package=mock --mock-name=PlaceService
   116  type Service interface {
   117  	Storer
   118  }
   119  
   120  // SortOrder defines the order of sorting.
   121  type SortOrder string
   122  
   123  // Possible values for SortOrder.
   124  const (
   125  	OrderAsc  = SortOrder("ASC")
   126  	OrderDesc = SortOrder("DESC")
   127  )
   128  
   129  // FindFilters defines the possible filters for the find method.
   130  type FindFilters struct {
   131  	ID          *string  `json:"id"`
   132  	NotID       *string  `json:"notID"`
   133  	Deactivated *bool    `json:"deactivated"`
   134  	Published   *bool    `json:"published"`
   135  	Name        *string  `json:"name"`
   136  	LikeName    *string  `json:"likeName"`
   137  	Description *string  `json:"description"`
   138  	OwnedBy     []string `json:"ownedBy"`
   139  }
   140  
   141  // Storer defines a service for CRUD operations on the event model.
   142  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/place Storer --output=../../internal/mock/place_store.go --package=mock --mock-name=PlaceStore
   143  type Storer interface {
   144  	Create(ctx context.Context, newPlace *NewPlace) (*Place, error)
   145  	Update(ctx context.Context, place *Place) (*Place, error)
   146  	Delete(ctx context.Context, id string) error
   147  	FindByID(ctx context.Context, id string) (*Place, error)
   148  	Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Place, int, error)
   149  }
   150  
   151  // NewService returns a new place service.
   152  func NewService(store Storer) Service {
   153  	return &service{store: store}
   154  }
   155  
   156  type service struct {
   157  	store Storer
   158  }
   159  
   160  var ErrNameAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("name already exists")}
   161  
   162  // Create makes sure the loggedin user is in the owned field.
   163  func (s *service) Create(ctx context.Context, newPlace *NewPlace) (*Place, error) {
   164  	if s.placeNameExists(ctx, newPlace.Name, "") {
   165  		return nil, ErrNameAlreadyExists
   166  	}
   167  	id, err := auth.UserIDFromContext(ctx)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	if !newPlace.IsOwned(id) {
   172  		newPlace.OwnedBy = append(newPlace.OwnedBy, id)
   173  	}
   174  	newPlace.Name = strings.TrimSpace(newPlace.Name)
   175  	return s.store.Create(ctx, newPlace)
   176  }
   177  
   178  func (s *service) Update(ctx context.Context, place *Place) (*Place, error) {
   179  	if s.placeNameExists(ctx, place.Name, place.ID) {
   180  		return nil, ErrNameAlreadyExists
   181  	}
   182  	return s.store.Update(ctx, place)
   183  }
   184  
   185  func (s *service) Delete(ctx context.Context, id string) error {
   186  	return s.store.Delete(ctx, id)
   187  }
   188  
   189  func (s *service) FindByID(ctx context.Context, id string) (*Place, error) {
   190  	return s.store.FindByID(ctx, id)
   191  }
   192  
   193  // Find adds a special filter value "self" for the OwnedBy field. If it is set
   194  // the value gets replaced with the id of the loggedin user.
   195  func (s *service) Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Place, int, error) {
   196  	if params != nil && params.Filters != nil && params.Filters.OwnedBy != nil {
   197  		if len(params.Filters.OwnedBy) == 1 && params.Filters.OwnedBy[0] == "self" {
   198  			id, err := auth.UserIDFromContext(ctx)
   199  			if err == nil {
   200  				params.Filters.OwnedBy = []string{id}
   201  			}
   202  		}
   203  	}
   204  	return s.store.Find(ctx, params)
   205  }
   206  
   207  func (s *service) placeNameExists(ctx context.Context, name string, id string) bool {
   208  	existingPlaces, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   209  		Filters: &FindFilters{Name: &name, NotID: &id},
   210  	})
   211  	if err != nil || len(existingPlaces) > 0 {
   212  		return true
   213  	}
   214  	return false
   215  }