Commit 5a737512 authored by Vadim Nesmashnov's avatar Vadim Nesmashnov

Merge branch 'develop' into...

Merge branch 'develop' into feature/RIHAKB-881-riha-haldajana-soovin-et-riha-funktsionaalsuse-manuaalsed-testid-oleks
parents 8d5572f5 fdadfbfe
image: riha-test-env
variables:
ARTIFACT_NAME: "ROOT.war"
stages:
- build
- deploy
build:
stage: build
script:
- ./build.sh
artifacts:
paths:
- backend/target/*.war
expire_in: 1 mos
tags:
- riha
deploy_demo:
stage: deploy
script:
- echo "$SSH_PRIVATE_KEY" > id_rsa
- chmod 700 id_rsa
- mkdir $HOME/.ssh
- echo "$SSH_HOST_KEY" > $HOME/.ssh/known_hosts
- scp -i id_rsa backend/target/*.war deployer@$SSH_HOST:$DEPLOYMENT_DIR/$ARTIFACT_NAME
environment:
name: demo
when: manual
tags:
- riha
\ No newline at end of file
language: java
jdk:
- oraclejdk8
before_script:
- npm install karma-cli karma jasmine-core karma-jasmine karma-junit-reporter karma-jasmine-jquery karma-jasmine-ajax karma-coverage karma-phantomjs2-launcher coffee-script@1.8.0 bower-installer@0.8.4
script: ./test.sh && ./build.sh
......@@ -66,7 +66,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.10</version>
<version>2.9.10.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
......@@ -95,9 +95,9 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
......@@ -173,35 +173,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>3.1.5</version>
<configuration>
<apiSources>
<apiSource>
<springmvc>true</springmvc>
<info>
<title>RIHA-Browser API</title>
<version>1.0</version>
<description>RIHA-Browser API documentation</description>
<license>
<name>MIT</name>
</license>
</info>
<schemes>
<scheme>http</scheme>
</schemes>
<basePath>/</basePath>
<locations>
<location>ee.ria.riha.web</location>
</locations>
<swaggerDirectory>${basedir}/src/main/resources/static</swaggerDirectory>
<outputFormats>yaml</outputFormats>
</apiSource>
</apiSources>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
......
......@@ -3,9 +3,11 @@ package ee.ria.riha.conf;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.fge.jackson.JsonLoader;
import ee.ria.riha.domain.model.NationalHolidays;
import ee.ria.riha.service.JsonValidationService;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
......@@ -51,4 +53,11 @@ public class ApplicationConfiguration {
File holidaysFile = new File(getClass().getClassLoader().getResource(applicationProperties.getNationalHolidaysFile()).toURI());
return mapper.readValue(holidaysFile, NationalHolidays.class);
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components())
.info(new Info().title("RIHA API").description("API description generated by springdoc-openapi plugin"));
}
}
......@@ -541,4 +541,19 @@ public class InfoSystem {
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InfoSystem that = (InfoSystem) o;
return id != null ? id.equals(that.id) : that.id == null;
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
package ee.ria.riha.domain.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
import java.time.LocalDate;
import java.util.Date;
......@@ -20,6 +17,7 @@ import static ee.ria.riha.domain.model.IssueEntityType.ISSUE;
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Issue implements IssueEntity {
private Long id;
......
package ee.ria.riha.domain.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
import java.util.Date;
......@@ -18,6 +15,7 @@ import static ee.ria.riha.domain.model.IssueEntityType.ISSUE_COMMENT;
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IssueComment implements IssueEntity {
private Long id;
......
......@@ -28,11 +28,11 @@ public class FileController {
@Autowired
private FileService fileService;
@PostMapping(API_V1_PREFIX + "/systems/{reference}/files")
@PostMapping(value = API_V1_PREFIX + "/systems/{reference}/files", consumes = "multipart/form-data")
@PreAuthorizeInfoSystemOwnerOrReviewer
@ApiOperation("Upload file")
public ResponseEntity upload(@PathVariable("reference") String reference,
@RequestParam("file") MultipartFile file) throws IOException {
@RequestPart("file") MultipartFile file) throws IOException {
log.info("Receiving info system '{}' file '{}' [{}] with size {}b",
reference, file.getOriginalFilename(), file.getContentType(), file.getSize());
......
......@@ -18,6 +18,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import static ee.ria.riha.conf.ApplicationProperties.API_V1_PREFIX;
import static java.util.stream.Collectors.toList;
......@@ -53,6 +56,46 @@ public class InfoSystemController {
infoSystemService.list(pageable, filterable),
infoSystemModelMapper));
}
@GetMapping("/autocomplete")
@ApiOperation("List all existing information systems for autocomplete")
@ApiPageableAndFilterableParams
public ResponseEntity autocomplete(Pageable pageable, Filterable filterable) {
// search for exact matches
PagedResponse<InfoSystem> exactMatches = infoSystemService.list(pageable, createExactMatchFilterFromILikeFilter(filterable));
if (exactMatches != null && exactMatches.getTotalElements() >= pageable.getPageSize()) {
// there are more exact matches than requested.
return ResponseEntity.ok(createPagedModel(exactMatches, infoSystemModelMapper));
} else if (exactMatches != null && exactMatches.getTotalElements() < pageable.getPageSize()) {
// there are some exact matches, need to fetch fuzzy matches
LinkedHashSet<InfoSystem> joinedList = new LinkedHashSet<>(exactMatches.getContent());
PagedResponse<InfoSystem> fuzzyMatches = infoSystemService.list(pageable, filterable);
if (fuzzyMatches != null && fuzzyMatches.getContent() != null) {
joinedList.addAll(fuzzyMatches.getContent());
}
return ResponseEntity.ok(createPagedModel(
new PagedResponse<>(
pageable,
joinedList.size(),
new ArrayList<>(joinedList)),
infoSystemModelMapper));
} else {
return ResponseEntity.ok(
createPagedModel(
infoSystemService.list(pageable, filterable),
infoSystemModelMapper));
}
}
private Filterable createExactMatchFilterFromILikeFilter(Filterable filterable) {
return new FilterRequest(
filterable.getFilter().replaceAll("%", ""),
filterable.getSort(),
filterable.getFields());
}
@GetMapping(path = "/data-objects")
@ApiOperation("List all existing information systems data objects")
......
package ee.ria.riha.web;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import ee.ria.riha.authentication.RihaUserDetails;
import ee.ria.riha.service.UserService;
import ee.ria.riha.storage.util.*;
import ee.ria.riha.web.model.UserDetailsModel;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
......@@ -17,17 +15,11 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ee.ria.riha.service.UserService;
import ee.ria.riha.storage.util.ApiPageableAndCompositeRequestParams;
import ee.ria.riha.storage.util.CompositeFilterRequest;
import ee.ria.riha.storage.util.PageRequest;
import ee.ria.riha.storage.util.Pageable;
import ee.ria.riha.storage.util.PagedResponse;
import ee.ria.riha.web.model.UserDetailsModel;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static ee.ria.riha.conf.ApplicationProperties.API_V1_PREFIX;
......@@ -38,16 +30,18 @@ import static ee.ria.riha.conf.ApplicationProperties.API_V1_PREFIX;
public class OrganizationController {
private static final String SORT_DELIMITER = "-";
private static final Map<String, Function<UserDetailsModel, ? extends Comparable>> sortFunctions;
static final Map<String, Comparator<UserDetailsModel>> USER_DETAILS_COMPARATORS;
static {
sortFunctions = new HashMap<>();
sortFunctions.put("firstName", UserDetailsModel::getFirstName);
sortFunctions.put("lastName", UserDetailsModel::getLastName);
sortFunctions.put("email", UserDetailsModel::getEmail);
sortFunctions.put("approver", UserDetailsModel::getApprover);
sortFunctions.put("producer", UserDetailsModel::getProducer);
USER_DETAILS_COMPARATORS = new HashMap<>();
USER_DETAILS_COMPARATORS.put("firstName", Comparator.comparing(UserDetailsModel::getFirstName, Comparator.nullsLast(String::compareToIgnoreCase)));
USER_DETAILS_COMPARATORS.put("lastName", Comparator.comparing(UserDetailsModel::getLastName, Comparator.nullsLast(String::compareToIgnoreCase)));
USER_DETAILS_COMPARATORS.put("email", Comparator.comparing(UserDetailsModel::getEmail, Comparator.nullsLast(String::compareToIgnoreCase)));
USER_DETAILS_COMPARATORS.put("approver", Comparator.comparing(UserDetailsModel::getApprover, Comparator.nullsLast(Boolean::compareTo)));
USER_DETAILS_COMPARATORS.put("producer", Comparator.comparing(UserDetailsModel::getProducer, Comparator.nullsLast(Boolean::compareTo)));
}
@Autowired
private UserService userService;
......@@ -64,7 +58,15 @@ public class OrganizationController {
List<UserDetailsModel> users = userService.getUsersByOrganization(rihaUserDetails.getActiveOrganization().getCode());
int totalUsers = users.size();
sortUsers(filterRequest, users);
String sortParameter = getSortFieldFromFilterRequest(filterRequest);
Comparator<UserDetailsModel> sortFunction =
sortParameter != null
? USER_DETAILS_COMPARATORS.get(sortParameter.replace(SORT_DELIMITER, ""))
: null;
if (sortParameter != null && sortFunction != null) {
sortUsers(users, sortFunction, sortParameter.startsWith(SORT_DELIMITER));
}
users = applyPaging(pageable, users);
return ResponseEntity.ok(new PagedResponse(
......@@ -73,17 +75,14 @@ public class OrganizationController {
users));
}
private void sortUsers(CompositeFilterRequest filterRequest, List<UserDetailsModel> users) {
List<String> sortParameters = filterRequest.getSortParameters();
if (!sortParameters.isEmpty()) {
String sort = sortParameters.get(0);
Function<UserDetailsModel, ? extends Comparable> sortFunction = sortFunctions.get(sort.replace(SORT_DELIMITER, ""));
if (sortFunction == null) {
return;
}
Comparator<UserDetailsModel> comparator = Comparator.comparing(sortFunction);
users.sort(sort.startsWith(SORT_DELIMITER) ? comparator.reversed() : comparator);
}
private String getSortFieldFromFilterRequest(CompositeFilterRequest filterRequest) {
return filterRequest != null && filterRequest.getSortParameters() != null && !filterRequest.getSortParameters().isEmpty()
? filterRequest.getSortParameters().get(0)
: null;
}
static void sortUsers(List<UserDetailsModel> users, Comparator<UserDetailsModel> sortFunction, boolean reverseSort) {
users.sort(reverseSort ? sortFunction.reversed() : sortFunction);
}
private List<UserDetailsModel> applyPaging(Pageable pageable, List<UserDetailsModel> users) {
......
......@@ -4,6 +4,7 @@ import ee.ria.riha.domain.model.IssueResolutionType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Model of an issue approval decision request
......@@ -13,6 +14,7 @@ import lombok.Data;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IssueApprovalDecisionModel {
private String comment;
private IssueResolutionType decisionType;
......
......@@ -3,6 +3,7 @@ package ee.ria.riha.web.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Model of an issue comment request
......@@ -12,6 +13,7 @@ import lombok.Data;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IssueCommentModel {
private String comment;
}
......@@ -5,6 +5,7 @@ import ee.ria.riha.domain.model.IssueStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Model of an issue status update request
......@@ -14,6 +15,7 @@ import lombok.Data;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IssueStatusUpdateModel {
private String comment;
private IssueStatus status;
......
package ee.ria.riha.web.model;
import ee.ria.riha.domain.model.RelationType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
/**
* Model of info system relationship request.
......@@ -15,6 +12,7 @@ import lombok.Setter;
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RelationModel {
private Long id;
private String infoSystemShortName;
......
package ee.ria.riha.web.model;
import ee.ria.riha.domain.model.RelationType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
import java.util.UUID;
......@@ -17,6 +14,7 @@ import java.util.UUID;
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RelationSummaryModel {
private Long id;
private UUID infoSystemUuid;
......
# RIHA-Browser configuration
#OpenAPI v3 location
springdoc.api-docs.path=/api-docs
## Force HTTP Encoding
spring.http.encoding.charset=UTF-8
......
package ee.ria.riha.web;
import ee.ria.riha.storage.util.CompositeFilterRequest;
import ee.ria.riha.web.model.UserDetailsModel;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.nullValue;
public class OrganizationControllerTest {
@Test
public void sortUsers() {
List<UserDetailsModel> testUsers = Arrays.asList(
UserDetailsModel.builder().email("test0@test.aa").build(),
UserDetailsModel.builder().email(null).build(),
UserDetailsModel.builder().email("test1@test.aa").build(),
UserDetailsModel.builder().email("test2@test.aa").build()
);
CompositeFilterRequest filterRequest = new CompositeFilterRequest(Collections.emptyList(), Collections.singletonList("asc"));
OrganizationController.sortUsers(testUsers, OrganizationController.USER_DETAILS_COMPARATORS.get("email"),false);
assertThat(testUsers.get(3).getEmail(), nullValue());
OrganizationController.sortUsers(testUsers, OrganizationController.USER_DETAILS_COMPARATORS.get("email"),true);
assertThat(testUsers.get(0).getEmail(), nullValue());
}
}
\ No newline at end of file
#!/bin/bash
mvn clean &&
pushd frontend &&
mvn frontend:install-node-and-yarn frontend:yarn frontend:yarn@install-bower frontend:yarn@build-production &&
popd &&
mvn package -DskipTests=true
\ No newline at end of file
......@@ -72,7 +72,6 @@ sudo npm install -g karma-jasmine-ajax
sudo npm install -g karma-coverage
sudo npm install -g karma-phantomjs2-launcher
sudo npm install -g coffee-script@1.8.0
sudo npm install -g bower-installer@0.8.4
~~~
## Build
......@@ -88,17 +87,6 @@ Frontend is a Angular 4 module generated by Angular CLI version 1.0.4. It can be
mvn frontend:install-node-and-yarn frontend:yarn
~~~
#### Install bower
Build process requires bower. Please note that bower can't run as root user or using sudo. If you absolutely have to, add `{ "allow_root": true }` to `~/.bowerrc`.
~~~bash
echo "{ \"allow_root\": true }" > ~/.bowerrc
~~~
Install bower
~~~bash
mvn frontend:yarn@install-bower
~~~
#### Build using maven
Build project using maven `frontend:yarn` task and calling `build` script from `package.json`. Compiled artifacts will be outputted to the `dist` directory.
~~~bash
......
......@@ -21,6 +21,7 @@
"src/favicon.ico"
],
"styles": [
"src/styles/fonts.scss",
"src/styles.css",
"src/styles/app.scss",
"./node_modules/font-awesome/css/font-awesome.css"
......
......@@ -26,9 +26,10 @@ export class AppComponent {
// 'en' not supported yet
translate.use('et');
const googleAnalyticsId = this.environmentService.globalEnvironment.getGoogleAnalyticsId();
const googleAnalyticsId = this.environmentService.globalEnvironment ?
this.environmentService.globalEnvironment.getGoogleAnalyticsId() : null;
this.router.routeReuseStrategy.shouldReuseRoute = function(future, curr){
this.router.routeReuseStrategy.shouldReuseRoute = function(future, curr) {
return false;
};
......
......@@ -4,21 +4,22 @@
</div>
<table id="infosystems-table" class="table table-striped table-bordered dataTable" cellspacing="0" width="100%">
<thead class="thead-light">
<tr>
<th>
<span class="btn-sm btn text-nowrap">Staatus</span>
</th>
<span class="btn-sm btn btn-primary text-nowrap">Staatus</span>
<th>
<span class="btn-sm btn text-nowrap">Infosüsteemi nimi</span>
<span class="btn-sm btn btn-primary text-nowrap">Infosüsteemi nimi</span>
</th>
<th>
<span class="btn-sm btn text-nowrap">Lühinimi</span>
<span class="btn-sm btn btn-primary text-nowrap">Lühinimi</span>
</th>
<th>
<span class="btn-sm btn text-nowrap">Pealkiri</span>
<span class="btn-sm btn btn-primary text-nowrap">Pealkiri</span>
</th>
<th>
<span class="btn-sm btn text-nowrap">Viimane kommentaar <i class="fa fa-sort-desc" aria-hidden="true"></i></span>
<span class="btn-sm btn btn-primary text-nowrap">Viimane kommentaar <i class="fa fa-sort-desc" aria-hidden="true"></i></span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ad of gridData.content">
......
<div class="fixed-header">
Hindajate tagasiside
<button (click)="closeModal(commentForm)" class="btn btn-sm btn-default close-modal-btn pull-right"><i class="fa fa-remove" aria-hidden="true"></i></button>
<button (click)="closeModal(commentForm)" class="btn btn-sm btn-secondary close-modal-btn pull-right"><i class="fa fa-remove" aria-hidden="true"></i></button>
</div>
<section class="col card p-3 main-content scrollable-modal-content">
<div class="my-1">
......
<section>
<div class="card-deck mb-2 mb-sm-3">
<div *ngFor="let card of cards" class="card feature">
<div class="card-block">
<div class="card-body">
<h4 class="card-title">
<span class="fa" [ngClass]="card.iconType">
</span>{{card.title}}</h4>
......
<div class="form-group row">
<label class="col-form-label advanced-label"><span class="text-muted"><i class="fa {{ icon }}" aria-hidden="true" style="margin-right:15px;"></i>{{ text }}</span> <strong>{{ date | date:'y-MM-dd'}}</strong></label>
<label class="col-form-label advanced-label"><span class="text-muted"><i class="fa {{ icon }}" aria-hidden="true" style="margin-right:15px;"></i>{{ text }} </span><span><strong>{{ date | date:'y-MM-dd'}}</strong></span></label>
</div>
......@@ -17,7 +17,7 @@
<div class="card-title">
Sisenege TARA kaudu
</div>
<div class="card-block">
<div class="card-body">
<div class="card-text">TARA autentimise alustamiseks vajutage “Jätka”</div>
</div>
<div class="card-footer">
......
......@@ -15,16 +15,25 @@
<p>
RIHAsse pannakse kirja riigi, kohaliku omavalitsuse või muu avalik-õigusliku juriidilise isiku või avalikke ülesandeid täitva eraõigusliku isiku infosüsteemid, mis asutatakse ja mida kasutatakse seaduses, selle alusel antud õigusaktis või rahvusvahelises lepingus sätestatud ülesannete täitmiseks.
</p>
<div class="form-group row" [ngClass]="{'has-danger': name.invalid && (name.dirty || name.touched || addForm.submitted)}">
<div class="form-group row">
<label for="name-input" class="col-sm-2 col-form-label pr-sm-0 text-sm-right required">Nimi:</label>
<div class="col-sm-10">
<input #name="ngModel" class="form-control" name="name" ngModel type="text" id="name-input" placeholder="nt. Andmeomanike ametlik register" required>
<input #name="ngModel"
[ngClass]="{'is-invalid': name.invalid && (name.dirty || name.touched || addForm.submitted)}"
class="form-control"
name="name"
ngModel
type="text"
id="name-input"
placeholder="nt. Andmeomanike ametlik register"
required>
</div>
</div>
<div class="form-group row" [ngClass]="{'has-danger': shortName.invalid && (shortName.dirty || shortName.touched || addForm.submitted)}">
<div class="form-group row">
<label for="short-name-input" class="col-sm-2 col-form-label pr-sm-0 text-sm-right required">Lühinimi:</label>
<div class="col-sm-10">
<input #shortName="ngModel"
[ngClass]="{'is-invalid': shortName.invalid && (shortName.dirty || shortName.touched || addForm.submitted)}"
class="form-control"
name="short_name"
pattern="^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)([a-zA-ZõÕäÄöÖüÜ0-9.-]*)$"
......@@ -33,10 +42,18 @@
placeholder="nt. AARE" required>
</div>
</div>
<div class="form-group row" [ngClass]="{'has-danger': purpose.invalid && (purpose.dirty || purpose.touched || addForm.submitted)}">
<div class="form-group row">
<label for="purpose-textarea" class="col-sm-2 col-form-label pr-sm-0 text-sm-right required">Infosüsteemi eesmärk:</label>
<div class="col-sm-10">
<textarea #purpose="ngModel" ngModel name="purpose" class="form-control counter-element" id="purpose-textarea" rows="10" placeholder="kellele ja miks infosüsteem luuakse" required></textarea>
<textarea #purpose="ngModel"
[ngClass]="{'is-invalid': purpose.invalid && (purpose.dirty || purpose.touched || addForm.submitted)}"
ngModel name="purpose"
class="form-control counter-element"
id="purpose-textarea"
rows="10"
placeholder="kellele ja miks infosüsteem luuakse"
required>
</textarea>
</div>
</div>
<button type="submit" class="btn btn-success pull-right">Salvesta</button>
......