1
0

feat(projects): remove namespaces

This commit is contained in:
kolaente
2022-12-29 16:40:06 +01:00
parent 0795828a9f
commit 16de7cd591
22 changed files with 317 additions and 3337 deletions

View File

@ -47,13 +47,14 @@ type Project struct {
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
NamespaceID int64 `xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace"`
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
ChildProjects []*Project `xorm:"-" json:"child_projects"`
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// Whether or not a project is archived.
// Whether a project is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
// The id of the file this project has set as background
@ -92,7 +93,7 @@ type ProjectWithTasksAndBuckets struct {
}
// TableName returns a better name for the projects table
func (l *Project) TableName() string {
func (p *Project) TableName() string {
return "projects"
}
@ -104,70 +105,42 @@ type ProjectBackgroundType struct {
// ProjectBackgroundUpload represents the project upload background type
const ProjectBackgroundUpload string = "upload"
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project{
// SharedProjectsPseudoProject is a pseudo project used to hold shared projects
var SharedProjectsPseudoProject = &Project{
ID: -1,
Title: "Favorites",
Description: "This project has all tasks marked as favorites.",
NamespaceID: FavoritesPseudoNamespace.ID,
IsFavorite: true,
Title: "Shared Projects",
Description: "Projects of other users shared with you via teams or directly.",
Created: time.Now(),
Updated: time.Now(),
}
// GetProjectsByNamespaceID gets all projects in a namespace
func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (projects []*Project, err error) {
switch nID {
case SharedProjectsPseudoNamespace.ID:
nnn, err := getSharedProjectsInNamespace(s, false, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case FavoritesPseudoNamespace.ID:
namespaces := make(map[int64]*NamespaceWithProjects)
_, err := getNamespacesWithProjects(s, &namespaces, "", false, 0, -1, doer.ID)
if err != nil {
return nil, err
}
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
ls, err := getProjectsForNamespaces(s, namespaceIDs, false)
if err != nil {
return nil, err
}
nnn, err := getFavoriteProjects(s, ls, namespaceIDs, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
case SavedFiltersPseudoNamespace.ID:
nnn, err := getSavedFilters(s, doer)
if err != nil {
return nil, err
}
if nnn != nil && nnn.Projects != nil {
projects = nnn.Projects
}
default:
err = s.Select("l.*").
Alias("l").
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
Where("l.is_archived = false").
Where("n.is_archived = false OR n.is_archived IS NULL").
Where("namespace_id = ?", nID).
Find(&projects)
}
if err != nil {
return nil, err
}
// FavoriteProjectsPseudoProject is a pseudo namespace used to hold favorite projects and tasks
var FavoriteProjectsPseudoProject = &Project{
ID: -2,
Title: "Favorites",
Description: "Favorite projects and tasks.",
Created: time.Now(),
Updated: time.Now(),
}
// get more project details
err = addProjectDetails(s, projects, doer)
return projects, err
// SavedFiltersPseudoProject is a pseudo namespace used to hold saved filters
var SavedFiltersPseudoProject = &Project{
ID: -3,
Title: "Filters",
Description: "Saved filters.",
Created: time.Now(),
Updated: time.Now(),
}
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project{
ID: -1,
Title: "Favorites",
Description: "This project has all tasks marked as favorites.",
ParentProjectID: FavoriteProjectsPseudoProject.ID,
IsFavorite: true,
Created: time.Now(),
Updated: time.Now(),
}
// ReadAll gets all projects a user has access to
@ -185,7 +158,7 @@ func GetProjectsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (proj
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects [get]
func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
@ -193,26 +166,67 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
if err != nil {
return nil, 0, 0, err
}
projects := []*Project{project}
projects := map[int64]*Project{}
projects[project.ID] = project
err = addProjectDetails(s, projects, a)
return projects, 0, 0, err
}
projects, resultCount, totalItems, err := getRawProjectsForUser(
doer, err := user.GetFromAuth(a)
if err != nil {
return nil, 0, 0, err
}
allProjects, resultCount, totalItems, err := getRawProjectsForUser(
s,
&projectOptions{
search: search,
user: &user.User{ID: a.GetID()},
page: page,
perPage: perPage,
isArchived: l.IsArchived,
search: search,
user: doer,
page: page,
perPage: perPage,
getArchived: p.IsArchived,
})
if err != nil {
return nil, 0, 0, err
}
// Add more project details
err = addProjectDetails(s, projects, a)
/////////////////
// Saved Filters
savedFiltersProject, err := getSavedFilterProjects(s, doer)
if err != nil {
return nil, 0, 0, err
}
if savedFiltersProject != nil {
allProjects[savedFiltersProject.ID] = savedFiltersProject
}
/////////////////
// Add project details (favorite state, among other things)
err = addProjectDetails(s, allProjects, a)
if err != nil {
return
}
//////////////////////////
// Putting it all together
var projects []*Project
for _, p := range allProjects {
if p.ParentProjectID != 0 {
if allProjects[p.ParentProjectID].ChildProjects == nil {
allProjects[p.ParentProjectID].ChildProjects = []*Project{}
}
allProjects[p.ParentProjectID].ChildProjects = append(allProjects[p.ParentProjectID].ChildProjects, p)
continue
}
// The projects variable will contain all projects which have no parents
// And because we're using the same pointers for everything, those will contain child projects
projects = append(projects, p)
}
return projects, resultCount, totalItems, err
}
@ -228,61 +242,61 @@ func (l *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [get]
func (l *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if l.ID == FavoritesPseudoProject.ID {
if p.ID == FavoritesPseudoProject.ID {
// Already "built" the project in CanRead
return nil
}
// Check for saved filters
if getSavedFilterIDFromProjectID(l.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(l.ID))
if getSavedFilterIDFromProjectID(p.ID) > 0 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(p.ID))
if err != nil {
return err
}
l.Title = sf.Title
l.Description = sf.Description
l.Created = sf.Created
l.Updated = sf.Updated
l.OwnerID = sf.OwnerID
p.Title = sf.Title
p.Description = sf.Description
p.Created = sf.Created
p.Updated = sf.Updated
p.OwnerID = sf.OwnerID
}
// Get project owner
l.Owner, err = user.GetUserByID(s, l.OwnerID)
p.Owner, err = user.GetUserByID(s, p.OwnerID)
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
if !l.IsArchived {
err = l.CheckIsArchived(s)
if !p.IsArchived {
err = p.CheckIsArchived(s)
if err != nil {
if !IsErrNamespaceIsArchived(err) && !IsErrProjectIsArchived(err) {
return
}
l.IsArchived = true
p.IsArchived = true
}
}
// Get any background information if there is one set
if l.BackgroundFileID != 0 {
if p.BackgroundFileID != 0 {
// Unsplash image
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, l.BackgroundFileID)
p.BackgroundInformation, err = GetUnsplashPhotoByFileID(s, p.BackgroundFileID)
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
return
}
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
}
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindProject)
p.IsFavorite, err = isFavorite(s, p.ID, a, FavoriteKindProject)
if err != nil {
return
}
l.Subscription, err = GetSubscription(s, SubscriptionEntityProject, l.ID, a)
p.Subscription, err = GetSubscription(s, SubscriptionEntityProject, p.ID, a)
return
}
@ -345,62 +359,32 @@ func GetProjectsByIDs(s *xorm.Session, projectIDs []int64) (projects map[int64]*
}
type projectOptions struct {
search string
user *user.User
page int
perPage int
isArchived bool
search string
user *user.User
page int
perPage int
getArchived bool
}
func getUserProjectsStatement(userID int64) *builder.Builder {
func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
dialect := config.DatabaseType.GetString()
if dialect == "sqlite" {
dialect = builder.SQLITE
}
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
Join("INNER", "namespaces n", "l.namespace_id = n.id").
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
Join("LEFT", "team_projects tl", "l.id = tl.project_id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
Where(builder.Or(
builder.Eq{"tm.user_id": userID},
builder.Eq{"tm2.user_id": userID},
builder.Eq{"ul.user_id": userID},
builder.Eq{"un.user_id": userID},
builder.Eq{"l.owner_id": userID},
)).
OrderBy("position").
GroupBy("l.id")
}
// Gets the projects only, without any tasks or so
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*Project, resultCount int, totalItems int64, err error) {
fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var isArchivedCond builder.Cond = builder.Eq{"1": 1}
if !opts.isArchived {
isArchivedCond = builder.And(
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
if !getArchived {
getArchivedCond = builder.And(
builder.Eq{"l.is_archived": false},
builder.Eq{"n.is_archived": false},
)
}
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
var filterCond builder.Cond
ids := []int64{}
if opts.search != "" {
vals := strings.Split(opts.search, ",")
if search != "" {
vals := strings.Split(search, ",")
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
@ -411,65 +395,114 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
}
}
filterCond = db.ILIKE("l.title", opts.search)
filterCond = db.ILIKE("l.title", search)
if len(ids) > 0 {
filterCond = builder.In("l.id", ids)
}
// Gets all Projects where the user is either owner or in a team which has access to the project
// Or in a team which has namespace read access
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
// TODO: remove namespaces
Join("INNER", "namespaces n", "l.namespace_id = n.id").
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
Join("LEFT", "team_projects tl", "l.id = tl.project_id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
Where(builder.And(
builder.Or(
builder.Eq{"tm.user_id": userID},
builder.Eq{"tm2.user_id": userID},
builder.Eq{"ul.user_id": userID},
builder.Eq{"un.user_id": userID},
builder.Eq{"l.owner_id": userID},
),
filterCond,
getArchivedCond,
)).
OrderBy("position").
GroupBy("l.id")
}
query := getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
if limit > 0 {
query = query.Limit(limit, start)
}
err = s.SQL(query).Find(&projects)
// Gets the projects with their children without any tasks
func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects map[int64]*Project, resultCount int, totalItems int64, err error) {
fullUser, err := user.GetUserByID(s, opts.user.ID)
if err != nil {
return nil, 0, 0, err
}
query = getUserProjectsStatement(fullUser.ID).
Where(filterCond).
Where(isArchivedCond)
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
// Gets all projects where the user is either owner or it was shared to them
allProjects := make(map[int64]*Project)
query := getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
if limit > 0 {
query = query.Limit(limit, start)
}
err = s.SQL(query).Find(&allProjects)
if err != nil {
return nil, 0, 0, err
}
query = getUserProjectsStatement(fullUser.ID, opts.search, opts.getArchived)
totalItems, err = s.
SQL(query.Select("count(*)")).
Count(&Project{})
return projects, len(projects), totalItems, err
if len(allProjects) == 0 {
return nil, 0, totalItems, nil
}
return projects, len(allProjects), totalItems, err
}
func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersNamespace *Project, err error) {
savedFilters, err := getSavedFiltersForUser(s, doer)
if err != nil {
return
}
if len(savedFilters) == 0 {
return nil, nil
}
savedFiltersPseudoNamespace := SavedFiltersPseudoProject
savedFiltersPseudoNamespace.OwnerID = doer.ID
*savedFiltersNamespace = *savedFiltersPseudoNamespace
savedFiltersNamespace.ChildProjects = make([]*Project, 0, len(savedFilters))
for _, filter := range savedFilters {
filterProject := filter.toProject()
filterProject.ParentProjectID = savedFiltersNamespace.ID
filterProject.Owner = doer
savedFiltersNamespace.ChildProjects = append(savedFiltersNamespace.ChildProjects, filterProject)
}
return
}
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
func addProjectDetails(s *xorm.Session, projects map[int64]*Project, a web.Auth) (err error) {
if len(projects) == 0 {
return
}
var ownerIDs []int64
for _, l := range projects {
ownerIDs = append(ownerIDs, l.OwnerID)
}
// Get all project owners
owners := map[int64]*user.User{}
if len(ownerIDs) > 0 {
err = s.In("id", ownerIDs).Find(&owners)
if err != nil {
return
}
}
var fileIDs []int64
var projectIDs []int64
for _, l := range projects {
projectIDs = append(projectIDs, l.ID)
if o, exists := owners[l.OwnerID]; exists {
l.Owner = o
}
if l.BackgroundFileID != 0 {
l.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
fileIDs = append(fileIDs, l.BackgroundFileID)
var fileIDs []int64
for _, p := range projects {
ownerIDs = append(ownerIDs, p.OwnerID)
projectIDs = append(projectIDs, p.ID)
fileIDs = append(fileIDs, p.BackgroundFileID)
}
owners, err := user.GetUsersByIDs(s, ownerIDs)
if err != nil {
return err
}
favs, err := getFavorites(s, projectIDs, a, FavoriteKindProject)
@ -479,19 +512,26 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
if err != nil {
log.Errorf("An error occurred while getting project subscriptions for a namespace item: %s", err.Error())
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
subscriptions = make(map[int64]*Subscription)
}
for _, project := range projects {
for _, p := range projects {
if o, exists := owners[p.OwnerID]; exists {
p.Owner = o
}
if p.BackgroundFileID != 0 {
p.BackgroundInformation = &ProjectBackgroundType{Type: ProjectBackgroundUpload}
}
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
if project.IsFavorite {
if p.IsFavorite {
continue
}
project.IsFavorite = favs[project.ID]
p.IsFavorite = favs[p.ID]
if subscription, exists := subscriptions[project.ID]; exists {
project.Subscription = subscription
if subscription, exists := subscriptions[p.ID]; exists {
p.Subscription = subscription
}
}
@ -521,46 +561,36 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
return
}
// NamespaceProject is a meta type to be able to join a project with its namespace
type NamespaceProject struct {
Project Project `xorm:"extends"`
Namespace Namespace `xorm:"extends"`
}
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
func (l *Project) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new project, we check if the namespace is archived
if l.ID == 0 {
n := &Namespace{ID: l.NamespaceID}
return n.CheckIsArchived(s)
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or any of its parent projects is archived.
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
// When creating a new project, we check if the parent is archived
if p.ID == 0 {
p := &Project{ID: p.ParentProjectID}
return p.CheckIsArchived(s)
}
nl := &NamespaceProject{}
exists, err := s.
Table("projects").
Join("LEFT", "namespaces", "projects.namespace_id = namespaces.id").
Where("projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)", l.ID).
Get(nl)
p, err := GetProjectSimpleByID(s, p.ID)
if err != nil {
return
return err
}
if exists && nl.Project.ID != 0 && nl.Project.IsArchived {
return ErrProjectIsArchived{ProjectID: l.ID}
}
if exists && nl.Namespace.ID != 0 && nl.Namespace.IsArchived {
return ErrNamespaceIsArchived{NamespaceID: nl.Namespace.ID}
// TODO: parent project
if p.IsArchived {
return ErrProjectIsArchived{ProjectID: p.ID}
}
return nil
}
func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) error {
if project.NamespaceID < 0 {
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.NamespaceID}
if project.ParentProjectID < 0 {
return &ErrProjectCannotBelongToAPseudoNamespace{ProjectID: project.ID, NamespaceID: project.ParentProjectID}
}
// Check if the namespace exists
if project.NamespaceID > 0 {
_, err := GetNamespaceByID(s, project.NamespaceID)
// Check if the parent project exists
if project.ParentProjectID > 0 {
_, err := GetProjectSimpleByID(s, project.ParentProjectID)
if err != nil {
return err
}
@ -635,19 +665,21 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth) (err error)
})
}
// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
// directly in the user.Create function.
func CreateNewProjectForUser(s *xorm.Session, user *user.User) (err error) {
p := &Project{
Title: "Inbox",
}
return p.Create(s, user)
}
func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProjectBackground bool) (err error) {
err = checkProjectBeforeUpdateOrDelete(s, project)
if err != nil {
return
}
if project.NamespaceID == 0 {
return &ErrProjectMustBelongToANamespace{
ProjectID: project.ID,
NamespaceID: project.NamespaceID,
}
}
// We need to specify the cols we want to update here to be able to un-archive projects
colsToUpdate := []string{
"title",
@ -721,27 +753,27 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [post]
func (l *Project) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromProjectID(l.ID)
func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
fid := getSavedFilterIDFromProjectID(p.ID)
if fid > 0 {
f, err := getSavedFilterSimpleByID(s, fid)
if err != nil {
return err
}
f.Title = l.Title
f.Description = l.Description
f.IsFavorite = l.IsFavorite
f.Title = p.Title
f.Description = p.Description
f.IsFavorite = p.IsFavorite
err = f.Update(s, a)
if err != nil {
return err
}
*l = *f.toProject()
*p = *f.toProject()
return nil
}
return UpdateProject(s, l, a, false)
return UpdateProject(s, p, a, false)
}
func updateProjectLastUpdated(s *xorm.Session, project *Project) error {
@ -773,13 +805,13 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/projects [put]
func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, l, a)
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, p, a)
if err != nil {
return
}
return l.ReadOne(s, a)
return p.ReadOne(s, a)
}
// Delete implements the delete method of CRUDable
@ -794,7 +826,7 @@ func (l *Project) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id} [delete]
func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
fullList, err := GetProjectSimpleByID(s, l.ID)
if err != nil {
@ -802,14 +834,14 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
// Delete the project
_, err = s.ID(l.ID).Delete(&Project{})
_, err = s.ID(p.ID).Delete(&Project{})
if err != nil {
return
}
// Delete all tasks on that project
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
tasks, _, _, err := getRawTasksForProjects(s, []*Project{l}, a, &taskOptions{})
tasks, _, _, err := getRawTasksForProjects(s, []*Project{p}, a, &taskOptions{})
if err != nil {
return
}
@ -827,7 +859,7 @@ func (l *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
return events.Dispatch(&ProjectDeletedEvent{
Project: l,
Project: p,
Doer: a,
})
}