diff --git a/app/Console/Commands/CertificatesIssues.php b/app/Console/Commands/CertificatesIssues.php index da5f2c03b..00d998f81 100644 --- a/app/Console/Commands/CertificatesIssues.php +++ b/app/Console/Commands/CertificatesIssues.php @@ -31,7 +31,7 @@ public function handle(): int { $issues = Participation::whereNull('participation_url') - ->where('status', '=', 'PENDING') + ->where('status', 'PENDING') ->where('created_at', '<', Carbon::now()->subMinutes(5))->get(); Log::info('certificate with issues: '.count($issues)); diff --git a/app/Console/Commands/CertificatesOfParticipationGeneration.php b/app/Console/Commands/CertificatesOfParticipationGeneration.php index f6ea73224..e61d1f4c5 100644 --- a/app/Console/Commands/CertificatesOfParticipationGeneration.php +++ b/app/Console/Commands/CertificatesOfParticipationGeneration.php @@ -28,7 +28,7 @@ class CertificatesOfParticipationGeneration extends Command public function handle(): int { - $participations = Participation::whereNull('participation_url')->where('status', '=', 'PENDING')->orderByDesc('created_at')->get(); + $participations = Participation::whereNull('participation_url')->where('status', 'PENDING')->orderByDesc('created_at')->get(); $this->info(count($participations).' certificates of participation to generate'); diff --git a/app/Console/Commands/EventsIndexOptimize.php b/app/Console/Commands/EventsIndexOptimize.php new file mode 100644 index 000000000..64822ce11 --- /dev/null +++ b/app/Console/Commands/EventsIndexOptimize.php @@ -0,0 +1,73 @@ +option('rollback'); + + $this->info(($rollback ? 'Dropping' : 'Adding') . ' indexes on events table...'); + + $indexes = [ + 'idx_status' => 'CREATE INDEX idx_status ON events(status)', + 'idx_country_iso' => 'CREATE INDEX idx_country_iso ON events(country_iso)', + 'idx_status_country' => 'CREATE INDEX idx_status_country ON events(status, country_iso)', + 'idx_start_date' => 'CREATE INDEX idx_start_date ON events(start_date)', + 'idx_end_date' => 'CREATE INDEX idx_end_date ON events(end_date)', + 'idx_activity_type' => 'CREATE INDEX idx_activity_type ON events(activity_type)', + 'idx_highlighted_status' => 'CREATE INDEX idx_highlighted_status ON events(highlighted_status)', + 'idx_lat_lon' => 'CREATE INDEX idx_lat_lon ON events(latitude, longitude)', + 'idx_creator_id' => 'CREATE INDEX idx_creator_id ON events(creator_id)', + 'idx_user_email' => 'CREATE INDEX idx_user_email ON events(user_email)', + 'idx_status_start' => 'CREATE INDEX idx_status_start ON events(status, start_date)', + ]; + + foreach ($indexes as $name => $sql) { + try { + if ($rollback) { + DB::statement("DROP INDEX {$name} ON events"); + $this->info("Dropped index: {$name}"); + } else { + $exists = DB::select( + "SELECT COUNT(1) as count FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'events' AND index_name = ?", + [$name] + ); + + if (!empty($exists) && $exists[0]->count == 0) { + DB::statement($sql); + $this->info("Created index: {$name}"); + } else { + $this->line("Index already exists: {$name}"); + } + } + } catch (\Throwable $e) { + $this->error("Failed to " . ($rollback ? 'drop' : 'create') . " index {$name}: " . $e->getMessage()); + } + } + + $this->info('Index operation completed.'); + return 0; + } +} diff --git a/app/Console/Commands/Excellence.php b/app/Console/Commands/Excellence.php index 647611feb..d564fc037 100644 --- a/app/Console/Commands/Excellence.php +++ b/app/Console/Commands/Excellence.php @@ -47,7 +47,7 @@ public function handle(): void //Select the winners from the Database $winners = []; foreach ($codeweek4all_codes as $codeweek4all_code) { - $creators = Event::whereYear('end_date', '=', $edition)->where('status', '=', 'APPROVED')->where('codeweek_for_all_participation_code', '=', $codeweek4all_code)->pluck('creator_id'); + $creators = Event::whereYear('end_date', '=', $edition)->where('status', 'APPROVED')->where('codeweek_for_all_participation_code', '=', $codeweek4all_code)->pluck('creator_id'); foreach ($creators as $creator) { if (! in_array($creator, $winners)) { $winners[] = $creator; diff --git a/app/Console/Commands/LocationExtraction.php b/app/Console/Commands/LocationExtraction.php index 2b59801aa..b6ebc31f1 100644 --- a/app/Console/Commands/LocationExtraction.php +++ b/app/Console/Commands/LocationExtraction.php @@ -32,7 +32,7 @@ public function handle(): void Event::whereNull('deleted_at')-> // where('id','=',163373)-> // where('creator_id',153701)-> - where('status', '=', 'APPROVED')-> + where('status', 'APPROVED')-> whereNull('location_id')->chunkById($this->step, function ($events, $index) { $this->reportProgress($index); diff --git a/app/Console/Commands/SyncEventAgesFromAudience.php b/app/Console/Commands/SyncEventAgesFromAudience.php new file mode 100644 index 000000000..362c22a03 --- /dev/null +++ b/app/Console/Commands/SyncEventAgesFromAudience.php @@ -0,0 +1,83 @@ + [1], // Pre-school children + '6-9' => [2], // Elementary school students + '10-12' => [2], // Upper primary → still Elementary + '13-15' => [3], // Lower secondary → High school + '16-18' => [3], // Upper secondary → High school + '19-25' => [4, 5], // Graduate & Post graduate students + 'over-25' => [6, 7, 9], // Employed + Unemployed adults + Teachers + ]; + + /** + * Execute the console command. + */ + public function handle() + { + $this->info('Syncing event.ages from audience_event...'); + + $map = self::AGE_AUDIENCE_MAP; + + $audienceToAges = []; + foreach ($map as $ageKey => $audienceIds) { + foreach ($audienceIds as $audienceId) { + $audienceToAges[$audienceId][] = $ageKey; + } + } + + $links = DB::table('audience_event') + ->select('event_id', 'audience_id') + ->get() + ->groupBy('event_id'); + + $updated = 0; + + foreach ($links as $eventId => $group) { + $ageKeys = collect($group) + ->flatMap(function ($row) use ($audienceToAges) { + return $audienceToAges[$row->audience_id] ?? []; + }) + ->unique() + ->values() + ->all(); + + if (!empty($ageKeys)) { + Event::where('id', $eventId)->update([ + 'ages' => json_encode($ageKeys), + ]); + $updated++; + } + } + + $this->info("Updated `ages` field for $updated events."); + return 0; + } +} diff --git a/app/Console/Commands/SyncThemesFromFinalList.php b/app/Console/Commands/SyncThemesFromFinalList.php new file mode 100644 index 000000000..0e83d0c9f --- /dev/null +++ b/app/Console/Commands/SyncThemesFromFinalList.php @@ -0,0 +1,267 @@ + 'AI & Generative AI', + 'Artificial intelligence' => 'AI & Generative AI', + + // Robotics, Drones, Devices + 'Robotics' => 'Robotics, Drones & Smart Devices', + 'Drones' => 'Robotics, Drones & Smart Devices', + 'Hardware' => 'Robotics, Drones & Smart Devices', + 'Digital Technologies' => 'Robotics, Drones & Smart Devices', + + // Web/App/Software Dev + 'Mobile app development' => 'Web, App & Software Development', + 'Web development' => 'Web, App & Software Development', + 'Software development' => 'Web, App & Software Development', + + // Game Design + 'Game design' => 'Game Design', + + // Cybersecurity & Data + 'Data manipulation and visualisation' => 'Cybersecurity & Data', // best match despite not direct + + // Visual/Block Programming + 'Basic programming concepts' => 'Visual/Block Programming', + 'Visual/Block programming' => 'Visual/Block Programming', + + // Art & Creative Coding + 'Art and creativity' => 'Art & Creative Coding', + + // Internet of Things & Wearables + 'Sensors' => 'Internet of Things & Wearables', + 'Internet of things and wearable computing' => 'Internet of Things & Wearables', + + // AR/VR/3D + '3D printing' => 'AR, VR & 3D Technologies', + 'Augmented reality' => 'AR, VR & 3D Technologies', + + // Digital Careers + 'Digital careers' => 'Digital Careers & Learning Pathways', + 'Digital learning pathways' => 'Digital Careers & Learning Pathways', + + // Soft Skills + 'Soft Skills' => 'Digital Literacy & Soft Skills', + + // Unplugged & Playful + 'Unplugged activities' => 'Unplugged & Playful Activities', + 'Playful coding activities' => 'Unplugged & Playful Activities', + + // Diversity + 'Promoting diversity' => 'Promoting Diversity & Inclusion', + + // Awareness + 'Motivation and awareness raising' => 'Awareness & Inspiration', + + // Other + 'Other' => 'Other', + ]; + + protected $finalThemes = [ + [17, 'AI & Generative AI', 15], + [6, 'Robotics, Drones & Smart Devices', 1], + [2, 'Web, App & Software Development', 4], + [13, 'Game Design', 10], + [5, 'Cybersecurity & Data', 2], + [1, 'Visual/Block Programming', 5], + [11, 'Art & Creative Coding', 8], + [14, 'Internet of Things & Wearables', 11], + [16, 'AR, VR & 3D Technologies', 12], + [3, 'Digital Careers & Learning Pathways', 13], + [4, 'Digital Literacy & Soft Skills', 14], + [9, 'Unplugged & Playful Activities', 6], + [19, 'Promoting Diversity & Inclusion', 17], + [18, 'Awareness & Inspiration', 16], + [8, 'Other', 18], + ]; + + /** + * Execute the console command. + */ + public function handle() + { + $restoreOnly = $this->option('restore'); + + if ($restoreOnly) { + return $this->restoreBackup(); + } + + $this->info('Starting themes sync and migration...'); + + DB::beginTransaction(); + + try { + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + config(['database.connections.mysql.strict' => false]); + DB::reconnect(); + + $eventThemeOld = DB::table('event_theme') + ->join('themes', 'event_theme.theme_id', '=', 'themes.id') + ->select('event_theme.event_id', 'themes.name as old_theme_name', 'event_theme.theme_id') + ->get(); + + if ( !Storage::disk('excel')->exists(self::BACKUP_FILE)) { + Storage::disk('excel')->put(self::BACKUP_FILE, $eventThemeOld->toJson(JSON_PRETTY_PRINT)); + $this->info('Backed up current event_theme to ' . self::BACKUP_FILE); + } + else { + $this->info('No update, Backed up current event_theme exist in ' . self::BACKUP_FILE); + } + + DB::table('event_theme')->delete(); + DB::table('themes')->delete(); + + $finalThemesMap = collect($this->finalThemes)->map(function ($theme) { + return [ + 'id' => $theme[0], + 'name' => $theme[1], + 'order' => $theme[2], + ]; + }); + + DB::table('themes')->insert($finalThemesMap->toArray()); + + // Set AUTO_INCREMENT to max(id) + 1 + $maxId = collect($this->finalThemes)->max(fn($theme) => $theme[0]); + DB::statement('ALTER TABLE themes AUTO_INCREMENT = ' . ($maxId + 1)); + + $newThemes = DB::table('themes')->get()->keyBy('name'); + + $mappedRows = []; + + foreach ($eventThemeOld as $item) { + $newName = self::OLD_TO_NEW_THEME_MAP[$item->old_theme_name] ?? null; + if ($newName && isset($newThemes[$newName])) { + $mappedRows[] = [ + 'event_id' => $item->event_id, + 'theme_id' => $newThemes[$newName]->id, + ]; + } + } + + $validatedRows = []; + + foreach (array_chunk($mappedRows, 500) as $chunk) { + $eventIds = array_column($chunk, 'event_id'); + + $existingIds = DB::table('events') + ->whereIn('id', $eventIds) + ->pluck('id') + ->toArray(); + + $validatedRows = array_merge($validatedRows, array_filter($chunk, fn($row) => in_array($row['event_id'], $existingIds))); + } + + $uniqueRows = collect($validatedRows) + ->unique(fn($row) => $row['event_id'] . '-' . $row['theme_id']) + ->values() + ->all(); + + foreach (array_chunk($uniqueRows, 1000) as $chunk) { + DB::table('event_theme')->insert($chunk); + } + + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + config(['database.connections.mysql.strict' => true]); + DB::reconnect(); + + DB::commit(); + + $this->info("Themes synced and event_theme migrated successfully. Total migrated: " . count($uniqueRows)); + } catch (\Throwable $e) { + DB::rollBack(); + $this->error("Error syncing themes: " . $e->getMessage()); + } + + return 0; + } + + protected function restoreBackup(): int + { + $this->info('Restoring event_theme from backup...'); + + if (!Storage::disk('excel')->exists(self::BACKUP_FILE)) { + $this->error('Backup file not found: ' . self::BACKUP_FILE); + return 1; + } + + $raw = Storage::disk('excel')->get(self::BACKUP_FILE); + $data = json_decode($raw, true); + $data = collect($data)->map(function ($row) { + return [ + 'event_id' => $row['event_id'], + 'theme_id' => $row['theme_id'], + ]; + })->values()->all(); + + if (empty($data)) { + $this->error('Backup is empty or invalid.'); + return 1; + } + + try { + config(['database.connections.mysql.strict' => false]); + DB::reconnect(); + + DB::beginTransaction(); + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + + DB::table('event_theme')->delete(); + DB::table('themes')->delete(); + + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + DB::commit(); + + $this->info('Running legacy ThemeTableSeeder...'); + $this->callSilent('db:seed', [ + '--class' => 'ThemeTableSeeder', + ]); + + DB::beginTransaction(); + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + + foreach (array_chunk($data, 1000) as $chunk) { + DB::table('event_theme')->insert($chunk); + } + + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + DB::commit(); + + config(['database.connections.mysql.strict' => true]); + DB::reconnect(); + + $this->info('Restored event_theme successfully: ' . count($data) . ' rows.'); + } catch (\Throwable $e) { + DB::rollBack(); + $this->error('Restore failed: ' . $e->getMessage()); + return 1; + } + + return 0; + } +} diff --git a/app/Event.php b/app/Event.php index 75fd24aad..5d419ec5c 100644 --- a/app/Event.php +++ b/app/Event.php @@ -16,9 +16,11 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Spatie\Activitylog\LogOptions; use Stevebauman\Purify\Casts\PurifyHtmlOnGet; +use Illuminate\Database\Eloquent\Casts\Attribute; class Event extends Model { @@ -67,6 +69,58 @@ class Event extends Model 'location_id', 'leading_teacher_tag', 'mass_added_for', + 'activity_format', + 'duration', + 'recurring_event', + 'recurring_type', + 'males_count', + 'females_count', + 'other_count', + 'is_extracurricular_event', + 'is_standard_school_curriculum', + 'is_use_resource', + 'ages' + ]; + + public const ACTIVITY_FORMATS = [ + 'coding-camp', + 'summer-camp', + 'weekend-course', + 'evening-course', + 'careerday', + 'university-visit', + 'coding-home', + 'code-week-challenge', + 'competition', + 'other', + ]; + + public const DURATIONS = [ + '0-1', + '1-2', + '2-4', + 'over-4', + ]; + + public const RECURRING_TYPES = [ + 'consecutive', + 'individual', + ]; + + public const RECURRING_EVENTS = [ + 'daily', + 'weekly', + 'monthly', + ]; + + public const AGES = [ + 'under-5', + '6-9', + '10-12', + '13-15', + '16-18', + '19-25', + 'over-25', ]; // protected $policies = [ @@ -74,7 +128,14 @@ class Event extends Model // Event::class => EventPolicy::class // ]; - //protected $appends = ['LatestModeration']; + protected $appends = ['picture_path', 'languages']; + + public function getLanguagesAttribute() { + if(!is_array($this->language)) { + return explode(',', $this->language) ?? null; + } + return $this->language; + } public function getUrlAttribute() { if (!empty($this->slug)) { @@ -108,7 +169,12 @@ protected function casts(): array 'description' => PurifyHtmlOnGet::class, 'title' => PurifyHtmlOnGet::class, 'location' => PurifyHtmlOnGet::class, - 'language' => PurifyHtmlOnGet::class, + + 'activity_format' => 'array', + 'is_extracurricular_event' => 'boolean', + 'is_standard_school_curriculum' => 'boolean', + 'ages' => 'array', + 'is_use_resource' => 'boolean', ]; } @@ -140,6 +206,8 @@ public function picture_path() return $this->picture; } + // For local test + // return Storage::disk('public')->url($this->picture); return config('codeweek.aws_url').$this->picture; } else { return 'https://s3-eu-west-1.amazonaws.com/codeweek-dev/events/pictures/event_default_picture.png'; @@ -212,7 +280,7 @@ public function scopeFilter($query, EventFilters $filters) public static function getByYear($year) { - $events = Event::where('status', 'like', 'APPROVED')->where( + $events = Event::where('status', 'APPROVED')->where( 'start_date', '>', Carbon::createFromDate($year, 1, 1) diff --git a/app/Filters/EventFilters.php b/app/Filters/EventFilters.php index 5686edfbb..cf153ba12 100755 --- a/app/Filters/EventFilters.php +++ b/app/Filters/EventFilters.php @@ -3,7 +3,6 @@ namespace App\Filters; use App\Tag; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -15,7 +14,20 @@ class EventFilters extends Filters * * @var array */ - protected $filters = ['countries', 'query', 'themes', 'audiences', 'types', 'year', 'creator_id', 'tag']; + protected $filters = [ + 'countries', + 'query', + 'themes', + 'audiences', + 'types', + 'year', + 'creator_id', + 'tag', + 'formats', + 'ages', + 'languages', + 'start_date' + ]; public function __construct(Request $request) { @@ -50,13 +62,11 @@ protected function countries($countries): Builder protected function year($year) { return $this->builder->whereYear('end_date', '=', $year); - } protected function creator_id($creator_id) { return $this->builder->where('creator_id', '=', $creator_id); - } protected function query($query) @@ -85,7 +95,6 @@ protected function themes($themes) return $this->builder ->leftJoin('event_theme', 'events.id', '=', 'event_theme.event_id') ->whereIn('event_theme.theme_id', $themesIds); - } protected function tag($tag) @@ -104,7 +113,6 @@ protected function tag($tag) return $this->builder ->leftJoin('event_tag', 'events.id', '=', 'event_tag.event_id') ->where('event_tag.tag_id', $selectedTag->id); - } protected function audiences($audiences) @@ -119,7 +127,6 @@ protected function audiences($audiences) return $this->builder ->leftJoin('audience_event', 'events.id', '=', 'audience_event.event_id') ->whereIn('audience_event.audience_id', $audiencesIds); - } protected function types($types) @@ -129,11 +136,65 @@ protected function types($types) return; } - $keys = collect($types)->pluck('key')->all(); + $keys = collect($types)->pluck('id')->all(); $result = $this->builder->whereIn('activity_type', $keys); return $result; + } + + protected function start_date($start_date) + { + if (!empty($start_date)) { + return $this->builder->where('start_date', '>=', $start_date); + } + return; + } + + protected function languages($languages) + { + if (empty($languages)) { + return; + } + + $keys = collect($languages)->pluck('id')->all(); + + $this->builder->where(function ($query) use ($keys) { + foreach ($keys as $lang) { + $query->orWhereRaw('FIND_IN_SET(?, language)', [$lang]); + } + }); + return $this->builder; + } + + protected function formats($formats) + { + if (empty($formats)) { + return; + } + + $keys = collect($formats)->pluck('id')->all(); + + $this->builder->where(function ($query) use ($keys) { + foreach ($keys as $key) { + $query->orWhereRaw("JSON_CONTAINS(activity_format, '\"$key\"')"); + } + }); + } + + protected function ages($ages) + { + if (empty($ages)) { + return; + } + + $keys = collect($ages)->pluck('id')->all(); + + $this->builder->where(function ($query) use ($keys) { + foreach ($keys as $key) { + $query->orWhereRaw("JSON_CONTAINS(ages, '\"$key\"')"); + } + }); } } diff --git a/app/Helpers/Codeweek4AllHelper.php b/app/Helpers/Codeweek4AllHelper.php index fe8745454..6500c58fa 100644 --- a/app/Helpers/Codeweek4AllHelper.php +++ b/app/Helpers/Codeweek4AllHelper.php @@ -17,7 +17,7 @@ public static function kpis($code, $edition = null) $result = Event::select(DB::raw('count(DISTINCT creator_id) as total_creators, sum(participants_count) as participants_count, count(id) as event_count, codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ['codeweek_for_all_participation_code', '=', $code], ]) ->whereYear('end_date', '=', $edition) @@ -39,7 +39,7 @@ public static function countries($code, $edition = null) select(DB::raw('count(DISTINCT creator_id) as total_creators, sum(participants_count) as total_participants, codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ]) ->whereYear('end_date', '=', $edition) ->groupBy('codeweek_for_all_participation_code') @@ -51,7 +51,7 @@ public static function countries($code, $edition = null) $result = Event::select(DB::raw('sum(participants_count) as participants_count, count(id) as event_count, codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ['codeweek_for_all_participation_code', '=', $code], ]) ->whereYear('end_date', '=', $edition) @@ -71,7 +71,7 @@ public static function getDetailsByCodeweek4All(array $toArray, $edition = null) return Event::select(DB::raw('codeweek_for_all_participation_code, sum(participants_count) as total_participants, count(DISTINCT creator_id) as total_creators, count(DISTINCT country_iso) as total_countries, count(id) as total_activities, ((100.0*count(reported_at))/count(*)) as reporting_percentage')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ]) ->whereYear('end_date', '=', $edition) ->whereIn('codeweek_for_all_participation_code', $toArray) @@ -88,7 +88,7 @@ public static function getCountriesByCodeweek4All($code, $edition = null) $result = Event::select(DB::raw('countries.name , count(events.id) as event_per_country')) ->join('countries', 'events.country_iso', '=', 'countries.iso') ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ['codeweek_for_all_participation_code', 'like', $code], ]) ->whereYear('end_date', '=', $edition) @@ -103,7 +103,7 @@ public static function getInitiatorByCodeweek4All($code) $result = Event::select(DB::raw('users.email')) ->join('users', 'events.creator_id', '=', 'users.id') ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ['codeweek_for_all_participation_code', 'like', $code], ]) ->orderBy('events.created_at', 'asc') diff --git a/app/Helpers/EventHelper.php b/app/Helpers/EventHelper.php index 378b3cc71..59639afa5 100644 --- a/app/Helpers/EventHelper.php +++ b/app/Helpers/EventHelper.php @@ -20,7 +20,7 @@ public static function getCloseEvents($longitude, $latitude, $id = 0, $num = 3) } $events = Event::selectRaw( - 'id, title, slug, start_date, end_date, picture, description, picture, creator_id, + 'id, title, slug, start_date, end_date, picture, description, picture, creator_id, highlighted_status, recurring_event, ( 6371 * acos( cos( radians(?) ) * cos( radians( latitude ) ) * @@ -31,7 +31,7 @@ public static function getCloseEvents($longitude, $latitude, $id = 0, $num = 3) AS distance', [$latitude, $longitude, $latitude] ) - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->where('id', '<>', $id) ->where('end_date', '>', Carbon::now()) ->orderBy('distance') @@ -55,7 +55,7 @@ public static function getPendindEvents() array_push($countries, $country->country_iso); } - $events = Event::where('status', '=', 'PENDING') + $events = Event::where('status', 'PENDING') ->distinct() ->select('country_iso') ->where('start_date', '>', Carbon::createFromDate(2018, 1, 1)) @@ -67,7 +67,7 @@ public static function getPendindEvents() public static function getReportedEventsWithoutCertificates() { - $events = Event::where('status', '=', 'APPROVED') + $events = Event::where('status', 'APPROVED') ->whereNotNull('reported_at') ->whereNull('certificate_url') ->whereNotNull('approved_by') @@ -81,7 +81,7 @@ public static function getReportedEventsWithoutCertificates() private static function getPendingEventsForCountry($country) { - $events = Event::where('status', '=', 'PENDING') + $events = Event::where('status', 'PENDING') ->where('start_date', '>', Carbon::createFromDate(2018, 1, 1)) ->where('country_iso', $country) ->get(); @@ -92,7 +92,7 @@ private static function getPendingEventsForCountry($country) private static function getPendingEventsCountForCountry($country) { - $count = Event::where('status', '=', 'PENDING') + $count = Event::where('status', 'PENDING') ->select('country_iso') ->where('start_date', '>', Carbon::createFromDate(2018, 1, 1)) ->where('country_iso', $country) @@ -103,7 +103,7 @@ private static function getPendingEventsCountForCountry($country) private static function getEventsQuery() { - return Event::where('status', '=', 'PENDING') + return Event::where('status', 'PENDING') ->where('start_date', '>', Carbon::createFromDate(2018, 1, 1)); } @@ -140,7 +140,7 @@ public static function getNextPendingEvent(Event $event, ?string $country = null return self::getEventsQuery()->where('id', '>', $event->id)->limit(1)->first(); } else { //Get pending events count for specific country - return Event::where('status', '=', 'PENDING') + return Event::where('status', 'PENDING') ->where('country_iso', $country) ->where('start_date', '>', Carbon::createFromDate(2018, 1, 1)) ->where('id', '<>', $event->id)->limit(1)->first(); diff --git a/app/Helpers/ExcellenceWinnersHelper.php b/app/Helpers/ExcellenceWinnersHelper.php index 0c902cd48..e6a9b550b 100644 --- a/app/Helpers/ExcellenceWinnersHelper.php +++ b/app/Helpers/ExcellenceWinnersHelper.php @@ -33,7 +33,7 @@ public static function criteria1($edition) $codes = Event::select(DB::raw('sum(participants_count) as total_participants, codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ]) ->whereYear('end_date', '=', $edition) ->groupBy('codeweek_for_all_participation_code') @@ -52,7 +52,7 @@ public static function criteria2($edition) $codes = Event::select(DB::raw('count(DISTINCT creator_id) as total_creators, sum(participants_count) as total_participants, codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ]) ->whereYear('end_date', '=', $edition) ->groupBy('codeweek_for_all_participation_code') @@ -72,7 +72,7 @@ public static function criteria3($edition) $codes = Event::select(DB::raw('count(DISTINCT country_iso) as total_countries, sum(participants_count) as total_participants,codeweek_for_all_participation_code')) ->where([ - ['status', 'like', 'APPROVED'], + ['status', 'APPROVED'], ]) ->whereYear('end_date', '=', $edition) ->groupBy('codeweek_for_all_participation_code') diff --git a/app/Helpers/MailingHelper.php b/app/Helpers/MailingHelper.php index 443b0e3e1..9aad7f671 100644 --- a/app/Helpers/MailingHelper.php +++ b/app/Helpers/MailingHelper.php @@ -11,7 +11,7 @@ public static function getActiveCreators($country) $activeIds = DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->where('users.receive_emails', true) ->where('events.country_iso', '=', $country) ->whereNull('users.deleted_at') @@ -25,7 +25,7 @@ public static function getActiveCreators($country) return DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->whereNull('events.deleted_at') ->whereIntegerInRaw('events.creator_id', $activeIds) ->groupBy('users.email') diff --git a/app/Helpers/ReminderHelper.php b/app/Helpers/ReminderHelper.php index 442fb665c..ee43f99cd 100644 --- a/app/Helpers/ReminderHelper.php +++ b/app/Helpers/ReminderHelper.php @@ -15,7 +15,7 @@ public static function getInactiveCreators($edition) $activeIds = DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') //->where('creator_id','=',$this->id) - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->where('users.receive_emails', true) ->whereNull('users.deleted_at') ->whereNull('events.deleted_at') @@ -29,7 +29,7 @@ public static function getInactiveCreators($edition) return DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') //->where('creator_id','=',$this->id) - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->whereNull('events.deleted_at') ->where(function ($query) use ($edition) { return $query->whereYear('events.end_date', '=', $edition - 1); @@ -49,7 +49,7 @@ public static function getActiveCreators() $activeIds = DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->where('users.receive_emails', true) ->whereNull('users.deleted_at') ->whereNull('events.deleted_at') @@ -62,7 +62,7 @@ public static function getActiveCreators() return DB::table('events') ->join('users', 'users.id', '=', 'events.creator_id') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->whereNull('events.deleted_at') ->whereIntegerInRaw('events.creator_id', $activeIds) ->groupBy('users.email') diff --git a/app/Http/Controllers/Api/EventsController.php b/app/Http/Controllers/Api/EventsController.php index 994df55ec..d80d1516d 100644 --- a/app/Http/Controllers/Api/EventsController.php +++ b/app/Http/Controllers/Api/EventsController.php @@ -105,7 +105,7 @@ public function germany(Request $request) ]); $collection = \App\Http\Resources\EventResource::collection( - Event::where('status', 'like', 'APPROVED') + Event::where('status', 'APPROVED') ->where('country_iso', 'DE') ->whereYear('end_date', '=', $validated['year']) ->get() @@ -158,7 +158,7 @@ public function geobox(Request $request) } $collection = \App\Http\Resources\EventResource::collection( - Event::where('status', 'like', 'APPROVED') + Event::where('status', 'APPROVED') ->where($box) ->whereYear('end_date', '=', $year) ->get() diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index 6ae8701ca..6774e65fb 100755 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -2,15 +2,13 @@ namespace App\Http\Controllers; -use App\Country; use App\Event; -use App\Helpers\EventHelper; use App\Http\Requests\EventRequest; use App\Queries\EventsQuery; use App\Queries\PendingEventsQuery; use App\User; -use Carbon\Carbon; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Arr; @@ -18,6 +16,7 @@ use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Mail; use Illuminate\View\View; +use Illuminate\Http\JsonResponse; class EventController extends Controller { @@ -26,7 +25,7 @@ class EventController extends Controller */ public function __construct() { - $this->middleware('auth')->except(['index', 'show', 'my']); + $this->middleware('auth')->except(['show', 'my']); } public function my(): View @@ -49,40 +48,6 @@ public function my(): View // // } - /** - * Display a listing of the resource. - */ - public function index(Request $request): View - { - $years = range(Carbon::now()->year, 2014, -1); - - $selectedYear = $request->input('year') - ? $request->input('year') - : Carbon::now()->year; - - $iso_country_of_user = User::getGeoIPData()->iso_code; - - $ambassadors = User::role('ambassador') - ->where('country_iso', '=', $iso_country_of_user) - ->get(); - - return view('events')->with([ - 'events' => $this->eventsNearMe(), - 'years' => $years, - 'selectedYear' => $selectedYear, - 'countries' => Country::withActualYearEvents(), - 'current_country_iso' => $iso_country_of_user, - 'ambassadors' => $ambassadors, - ]); - } - - private function eventsNearMe() - { - $geoip = User::getGeoIPData(); - - return EventHelper::getCloseEvents($geoip->lon, $geoip->lat); - } - /** * Show the form for creating a new resource. */ @@ -99,12 +64,16 @@ public function create(Request $request) $leading_teachers = $this->getLeadingTeachersTagsArray(); if ($request->get('location')) { - $location = auth()->user()->locations()->where('id', $request->get('location'))->firstOrFail(); - - return view('event.add', compact(['countries', 'themes', 'languages', 'location', 'leading_teachers'])); + try { + $location = auth()->user()->locations()->where('id', $request->get('location'))->firstOrFail(); + return view('event.add', compact(['countries', 'themes', 'languages', 'location', 'leading_teachers'])); + } + catch (ModelNotFoundException $e) { + return redirect(route('activities-locations')); + } + } - - if (! auth()->user()->locations->isEmpty()) { + else { if (! $request->get('skip')) { return redirect(route('activities-locations')); } @@ -123,7 +92,7 @@ public function search(): View /** * Store a newly created resource in storage. */ - public function store(EventRequest $request): View + public function store(EventRequest $request): JsonResponse { $user = auth()->user(); @@ -137,11 +106,14 @@ public function store(EventRequest $request): View $event->createLocation(); - Mail::to(auth()->user()->email)->queue( - new \App\Mail\EventRegistered($event, auth()->user()) + Mail::to($user->email)->queue( + new \App\Mail\EventRegistered($event, $user) ); - return view('event.thankyou', compact('event')); + return response()->json([ + 'message' => 'Event registered successfully.', + 'event' => $event, + ], 201); } /** @@ -186,7 +158,7 @@ public function edit(Event $event): View ->pluck('id') ->toArray(); $selected_audiences = implode(',', $selected_audiences); - $selected_country = $event->country()->first()->iso; + $selected_country = optional($event->country()->first())->iso; $selected_language = is_null($event->language) ? 'en' : $event->language; diff --git a/app/Http/Controllers/ExcellenceWinnersController.php b/app/Http/Controllers/ExcellenceWinnersController.php index 1222f2770..dd68a311d 100644 --- a/app/Http/Controllers/ExcellenceWinnersController.php +++ b/app/Http/Controllers/ExcellenceWinnersController.php @@ -35,14 +35,14 @@ public function list(Request $request, $edition = 2024): View $details = ExcellenceWinnersHelper::query($edition, false); $total_events = DB::table('events') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') //->where('codeweek_for_all_participation_code', '<>', 'cw19-apple-eu') ->whereYear('end_date', '=', $edition) ->whereNull('deleted_at') ->count(); $total_reported = DB::table('events') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->whereNotNull('reported_at') ->whereNull('deleted_at') ->whereYear('end_date', '=', $edition) diff --git a/app/Http/Controllers/ScoreboardController.php b/app/Http/Controllers/ScoreboardController.php index ecc6e62a0..9d802a7d2 100644 --- a/app/Http/Controllers/ScoreboardController.php +++ b/app/Http/Controllers/ScoreboardController.php @@ -38,7 +38,7 @@ public function index(Request $request) $total = Cache::remember('total_' . $edition, $cache_time, function () use ($edition) { Log::info("Setting cache for scoreboard total in " . $edition); return DB::table('events') - ->where('status', "=", "APPROVED") + ->where('status', "APPROVED") ->whereNull('deleted_at') ->whereYear('end_date', '=', $edition) ->count(); @@ -52,7 +52,7 @@ public function index(Request $request) $events = DB::table('events') ->join('countries', 'events.country_iso', '=', 'countries.iso') ->select('countries.iso as country_iso', 'countries.name as country_name', 'countries.population as country_population', DB::raw('count(*) as total')) - ->where('status', "=", "APPROVED") + ->where('status', "APPROVED") ->whereYear('end_date', '=', $edition) ->whereNull('deleted_at') ->where('countries.parent', "=", "") @@ -63,7 +63,7 @@ public function index(Request $request) $eventsFromDependencies = DB::table('events') ->join('countries', 'events.country_iso', '=', 'countries.iso') ->select('countries.population as population', 'countries.parent as iso', DB::raw('count(*) as total')) - ->where('status', "=", "APPROVED") + ->where('status', "APPROVED") ->whereNull('deleted_at') ->whereYear('end_date', '=', $edition) ->whereNotNull('countries.parent') diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 9b59747a4..265e93fa7 100755 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -60,74 +60,46 @@ public function search(Request $request): View public function searchPOST(EventFilters $filters, Request $request) { - $events = $this->getEvents($filters); + $paginatedEvents = $this->getPaginatedEvents($filters); - //Log::info($request->input('page')); if ($request->input('page')) { - $result = [$events]; - } else { - Log::info('no page'); - $eventsMap = $this->getAllEventsToMap($filters); - $result = [$events, $eventsMap]; + return [$paginatedEvents]; } - return $result; + $mapData = $this->transformEventsForMap($filters); + + return [$paginatedEvents, $mapData]; } - protected function getEvents(EventFilters $filters) + protected function getPaginatedEvents(EventFilters $filters) { - - $events = Event::where('status', 'like', 'APPROVED') + return Event::where('status', 'APPROVED') ->filter($filters) + ->orderByRaw("start_date > ? desc", [Carbon::today()]) ->orderBy('start_date') - ->get() - ->groupBy(function ($event) { - if ($event->start_date <= Carbon::today()) { - return 'past'; - } - - return 'future'; - }); - - if (is_null($events->get('future')) || is_null($events->get('past'))) { - return $events->flatten()->paginate(12); - } - - return $events->get('future')->merge($events->get('past'))->paginate(12); + ->paginate(12); } - protected function getAllEventsToMap(EventFilters $filters) + protected function transformEventsForMap(EventFilters $filters) { - $flattened = Arr::flatten($filters->getFilters()); $filtered = array_filter($flattened, fn($v) => $v !== null && $v !== ''); - $composed_key = implode(',', $filtered); + $composed_key = 'map_' . implode(',', $filtered); - if (empty($composed_key)) { - Log::info('Skipping cache due to empty composed_key'); - return Event::where('status', 'APPROVED') - ->filter($filters) - ->get() - ->groupBy('country'); - } + return Cache::remember($composed_key, 300, function () use ($filters) { + $grouped = []; - $value = Cache::get($composed_key, function () use ($composed_key, $filters) { - Log::info("Building cache [{$composed_key}]"); - $events = Event::where('status', 'like', 'APPROVED') + Event::select('id', 'geoposition', 'country_iso') // Only required fields + ->where('status', 'APPROVED') ->filter($filters) - ->get(); - - $events = $this->eventTransformer->transformCollection($events); - - $events = $events->groupBy('country'); - - Cache::put($composed_key, $events, 5 * 60); - - return $events; + ->cursor() + ->each(function ($event) use (&$grouped) { + $transformed = app(\App\Http\Transformers\EventTransformer::class)->transform($event); + $country = $transformed['country'] ?? 'unknown'; + $grouped[$country][] = $transformed; + }); + + return collect($grouped); }); - - Log::info("Serving from cache [{$composed_key}]"); - - return $value; } } diff --git a/app/Http/Requests/EventRequest.php b/app/Http/Requests/EventRequest.php index 3e37fd724..393b1c01a 100644 --- a/app/Http/Requests/EventRequest.php +++ b/app/Http/Requests/EventRequest.php @@ -6,6 +6,7 @@ use App\Rules\validTheme; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Lang; +use Illuminate\Validation\Rule; class EventRequest extends FormRequest { @@ -31,13 +32,18 @@ public function rules(): array 'title' => 'required|min:5', 'description' => 'required|min:5', 'organizer' => 'required', + 'duration' => 'required', 'location' => 'required_unless:activity_type,open-online,invite-online', 'event_url' => 'required_if:activity_type,open-online,invite-online', - 'language' => ['required', $this->in($languages)], + 'language' => ['required', 'array'], + 'language.*' => ['required', Rule::in($languages)], 'start_date' => 'required', 'end_date' => 'required|after:start_date', 'audience' => ['required', new ValidAudience], 'theme' => ['required', new ValidTheme], + 'participants_count' => 'required', + 'ages' => 'required', + 'is_extracurricular_event' => 'required|boolean', 'country_iso' => 'required|exists:countries,iso', 'user_email' => 'required', 'organizer_type' => 'required', @@ -50,14 +56,20 @@ public function rules(): array public function messages(): array { return [ + 'activity_type.required' => 'Please select an activity type.', 'title.required' => 'Please enter a title for your event.', 'description.required' => 'Please write a short description of what the event is about.', 'organizer.required' => 'Please enter an organizer.', + 'duration.required' => 'Please specify the event duration.', 'location.required' => 'Please enter a location.', 'start_date.required' => 'Please enter a valid date and time (example: 2014-10-22 18:00).', 'end_date.required' => 'Please enter a valid date and time (example: 2014-10-22 18:00).', 'audience.required' => 'If unsure, choose Other and provide more information in the description.', 'theme.required' => 'If unsure, choose Other and provide more information in the description.', + 'participants_count.required' => 'Please specify the expected number of participants.', + 'ages.required' => 'Please select at least one age group.', + 'is_extracurricular_event.required' => 'Please specify if this is an extracurricular event.', + 'is_extracurricular_event.boolean' => 'The extracurricular event field must be true or false.', 'country.required' => 'The event\'s location should be in Europe.', 'event_url.url' => 'The activity\'s web page address should be a valid URL.', 'event_url.required' => 'The activity\'s web page is required for online activities.', diff --git a/app/Http/Resources/EventResource.php b/app/Http/Resources/EventResource.php index 8473ce1b5..5293a93f2 100644 --- a/app/Http/Resources/EventResource.php +++ b/app/Http/Resources/EventResource.php @@ -29,6 +29,7 @@ public function toArray(Request $request): array 'event_url' => $this->event_url, 'contact_person' => $this->contact_person, 'language' => $this->language, + 'languages' => $this->languages, 'imported_from_german_feeds' => $this->imported(), 'codeweek_for_all_participation_code' => $this->codeweek_for_all_participation_code, 'themes' => ThemeResource::collection($this->themes), diff --git a/app/Imports/AppleEventsImport.php b/app/Imports/AppleEventsImport.php index 229afaa3c..4a7f7e0da 100644 --- a/app/Imports/AppleEventsImport.php +++ b/app/Imports/AppleEventsImport.php @@ -7,16 +7,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; -use PhpOffice\PhpSpreadsheet\Shared\Date; -class AppleEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class AppleEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { - public function parseDate($date) - { - return Date::excelToDateTimeObject($date); - } - public function model(array $row): ?Model { @@ -45,12 +38,50 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower(explode('_', $row['language'])[0]), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } return $event; diff --git a/app/Imports/AvandeEventsImport.php b/app/Imports/AvandeEventsImport.php index 7ab7b435b..9e2a4e690 100644 --- a/app/Imports/AvandeEventsImport.php +++ b/app/Imports/AvandeEventsImport.php @@ -9,10 +9,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class AvandeEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class AvandeEventsImport extends AppleEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -72,6 +71,41 @@ public function model(array $row): ?Model 'language' => !empty($row['language']) ? strtolower(explode('_', $row['language'])[0]) : 'en', 'approved_by' => 19588, 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -88,14 +122,9 @@ public function model(array $row): ?Model } // Handle themes with validation - if (!empty($row['theme_comma_separated_ids'])) { - $themes = array_unique(array_map('trim', explode(',', $row['theme_comma_separated_ids']))); - $themes = array_filter($themes, function ($id) { - return is_numeric($id) && $id > 0 && $id <= 100; - }); - if (!empty($themes)) { - $event->themes()->attach($themes); - } + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); } return $event; diff --git a/app/Imports/BaseEventsImport.php b/app/Imports/BaseEventsImport.php new file mode 100644 index 000000000..14ab11693 --- /dev/null +++ b/app/Imports/BaseEventsImport.php @@ -0,0 +1,66 @@ +map(fn($v) => trim($v)) + ->filter(fn($v) => in_array($v, $valid, true)) + ->values() + ->all(); + } + + protected function validateSingleChoice(?string $input, array $valid): ?string + { + return in_array($input, $valid, true) ? $input : null; + } + + protected function validateThemes(string $input): array + { + if (empty($input)) { + return []; + } + + $themeIds = array_unique(array_filter(array_map('trim', explode(',', $input)))); + + // Mapping deleted old id to new id group + $themeIdMapping = [ + 7 => 6, + 10 => 9, + 12 => 1, + 15 => 16, + ]; + + $mappedThemeIds = array_map(function ($id) use ($themeIdMapping) { + return $themeIdMapping[$id] ?? $id; + }, $themeIds); + + $validThemeIds = Theme::whereIn('id', $mappedThemeIds)->pluck('id')->toArray(); + + return $validThemeIds; + } +} diff --git a/app/Imports/BulgariaEventsImport.php b/app/Imports/BulgariaEventsImport.php index 3f50d5184..a786b12b5 100644 --- a/app/Imports/BulgariaEventsImport.php +++ b/app/Imports/BulgariaEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class BulgariaEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class BulgariaEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -48,12 +47,50 @@ public function model(array $row): ?Model 'longitude' => $row['longitude'], 'latitude' => $row['latitude'], 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } return $event; diff --git a/app/Imports/CoderDojoEventsImport.php b/app/Imports/CoderDojoEventsImport.php index 367457081..21be8bbf2 100644 --- a/app/Imports/CoderDojoEventsImport.php +++ b/app/Imports/CoderDojoEventsImport.php @@ -3,15 +3,13 @@ namespace App\Imports; use App\Event; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; -class CoderDojoEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class CoderDojoEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -65,6 +63,41 @@ public function model(array $row): ?Model 'language' => !empty($row['language']) ? trim($row['language']) : 'nl', 'approved_by' => 19588, 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -79,14 +112,9 @@ public function model(array $row): ?Model } } - if (!empty($row['theme_comma_separated_ids'])) { - $themes = array_unique(array_map('trim', explode(',', $row['theme_comma_separated_ids']))); - $themes = array_filter($themes, function ($id) { - return is_numeric($id) && $id > 0 && $id <= 100; - }); - if (!empty($themes)) { - $event->themes()->attach($themes); - } + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); } return $event; diff --git a/app/Imports/CodingActivitiesEUCodeWeekSiteImport.php b/app/Imports/CodingActivitiesEUCodeWeekSiteImport.php index 413e1226b..526e97a2a 100644 --- a/app/Imports/CodingActivitiesEUCodeWeekSiteImport.php +++ b/app/Imports/CodingActivitiesEUCodeWeekSiteImport.php @@ -3,16 +3,13 @@ namespace App\Imports; use App\Event; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Log; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class CodingActivitiesEUCodeWeekSiteImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class CodingActivitiesEUCodeWeekSiteImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -71,12 +68,50 @@ public function model(array $row): ?Model 'language' => $row['language'], 'approved_by' => 19588, 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } return $event; diff --git a/app/Imports/DutchDanceEventsImport.php b/app/Imports/DutchDanceEventsImport.php index f0231de4a..79bd1fd22 100644 --- a/app/Imports/DutchDanceEventsImport.php +++ b/app/Imports/DutchDanceEventsImport.php @@ -10,10 +10,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class DutchDanceEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class DutchDanceEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -64,6 +63,41 @@ public function model(array $row): ?Model 'latitude' => $row['longitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -74,9 +108,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/DutchMoorlagEventsImport.php b/app/Imports/DutchMoorlagEventsImport.php index 344ea3465..87fa9629a 100644 --- a/app/Imports/DutchMoorlagEventsImport.php +++ b/app/Imports/DutchMoorlagEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class DutchMoorlagEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class DutchMoorlagEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -48,6 +47,41 @@ public function model(array $row): ?Model 'latitude' => $row['longitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -58,9 +92,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/DutchSimoneEventsImport.php b/app/Imports/DutchSimoneEventsImport.php index eed2bca19..489494f1a 100644 --- a/app/Imports/DutchSimoneEventsImport.php +++ b/app/Imports/DutchSimoneEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class DutchSimoneEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class DutchSimoneEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -48,6 +47,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -58,9 +92,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/EventiEventsImport.php b/app/Imports/EventiEventsImport.php index 6b7c7b566..24576d8e7 100644 --- a/app/Imports/EventiEventsImport.php +++ b/app/Imports/EventiEventsImport.php @@ -3,96 +3,126 @@ namespace App\Imports; use App\Event; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; -class EventiEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class EventiEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { - public function parseDate($date) - { - $arr = explode(',', $date); - array_shift($arr); - return implode($arr); - } - - public function model(array $row): ?Model - { - // Validate required fields - if ( - empty($row['activity_title']) || - empty($row['name_of_organisation']) || - empty($row['description']) || - empty($row['type_of_organisation']) || - empty($row['activity_type']) || - empty($row['country']) || - empty($row['start_date']) || - empty($row['end_date']) - ) { - Log::error('Missing required fields in row'); - return null; + public function parseDate($date) + { + $arr = explode(',', $date); + array_shift($arr); + return implode($arr); } - try { - $event = new Event([ - 'status' => 'APPROVED', - 'title' => trim($row['activity_title']), - 'slug' => str_slug(trim($row['activity_title'])), - 'organizer' => trim($row['name_of_organisation']), - 'description' => trim($row['description']), - 'organizer_type' => trim($row['type_of_organisation']), - 'activity_type' => trim($row['activity_type']), - 'location' => !empty($row['address']) ? trim($row['address']) : 'online', - 'event_url' => !empty($row['organiser_website']) ? trim($row['organiser_website']) : '', - 'contact_person' => !empty($row['contact_email']) ? trim($row['contact_email']) : '', - 'user_email' => '', - 'creator_id' => 132942, - 'country_iso' => trim($row['country']), - 'picture' => !empty($row['image_path']) ? trim($row['image_path']) : '', - 'pub_date' => now(), - 'created' => now(), - 'updated' => now(), - 'codeweek_for_all_participation_code' => 'cw20-coderdojo-eu', - 'start_date' => \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['start_date']), - 'end_date' => \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['end_date']), - 'geoposition' => (!empty($row['latitude']) && !empty($row['longitude'])) ? $row['latitude'] . ',' . $row['longitude'] : '', - 'longitude' => !empty($row['longitude']) ? trim($row['longitude']) : '', - 'latitude' => !empty($row['latitude']) ? trim($row['latitude']) : '', - 'language' => !empty($row['language']) ? trim($row['language']) : 'nl', - 'approved_by' => 19588, - 'mass_added_for' => 'Excel', - ]); - - $event->save(); - - if (!empty($row['audience_comma_separated_ids'])) { - $audiences = array_unique(array_map('trim', explode(',', $row['audience_comma_separated_ids']))); - $audiences = array_filter($audiences, function ($id) { - return is_numeric($id) && $id > 0 && $id <= 100; - }); - if (!empty($audiences)) { - $event->audiences()->attach($audiences); + public function model(array $row): ?Model + { + // Validate required fields + if ( + empty($row['activity_title']) || + empty($row['name_of_organisation']) || + empty($row['description']) || + empty($row['type_of_organisation']) || + empty($row['activity_type']) || + empty($row['country']) || + empty($row['start_date']) || + empty($row['end_date']) + ) { + Log::error('Missing required fields in row'); + return null; } - } - - if (!empty($row['theme_comma_separated_ids'])) { - $themes = array_unique(array_map('trim', explode(',', $row['theme_comma_separated_ids']))); - $themes = array_filter($themes, function ($id) { - return is_numeric($id) && $id > 0 && $id <= 100; - }); - if (!empty($themes)) { - $event->themes()->attach($themes); - } - } - return $event; - } catch (\Exception $e) { - Log::error('Event import failed: ' . $e->getMessage()); - return null; + try { + $event = new Event([ + 'status' => 'APPROVED', + 'title' => trim($row['activity_title']), + 'slug' => str_slug(trim($row['activity_title'])), + 'organizer' => trim($row['name_of_organisation']), + 'description' => trim($row['description']), + 'organizer_type' => trim($row['type_of_organisation']), + 'activity_type' => trim($row['activity_type']), + 'location' => !empty($row['address']) ? trim($row['address']) : 'online', + 'event_url' => !empty($row['organiser_website']) ? trim($row['organiser_website']) : '', + 'contact_person' => !empty($row['contact_email']) ? trim($row['contact_email']) : '', + 'user_email' => '', + 'creator_id' => 132942, + 'country_iso' => trim($row['country']), + 'picture' => !empty($row['image_path']) ? trim($row['image_path']) : '', + 'pub_date' => now(), + 'created' => now(), + 'updated' => now(), + 'codeweek_for_all_participation_code' => 'cw20-coderdojo-eu', + 'start_date' => \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['start_date']), + 'end_date' => \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($row['end_date']), + 'geoposition' => (!empty($row['latitude']) && !empty($row['longitude'])) ? $row['latitude'] . ',' . $row['longitude'] : '', + 'longitude' => !empty($row['longitude']) ? trim($row['longitude']) : '', + 'latitude' => !empty($row['latitude']) ? trim($row['latitude']) : '', + 'language' => !empty($row['language']) ? trim($row['language']) : 'nl', + 'approved_by' => 19588, + 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, + ]); + + $event->save(); + + if (!empty($row['audience_comma_separated_ids'])) { + $audiences = array_unique(array_map('trim', explode(',', $row['audience_comma_separated_ids']))); + $audiences = array_filter($audiences, function ($id) { + return is_numeric($id) && $id > 0 && $id <= 100; + }); + if (!empty($audiences)) { + $event->audiences()->attach($audiences); + } + } + + if (!empty($row['theme_comma_separated_ids'])) { + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } + } + + return $event; + } catch (\Exception $e) { + Log::error('Event import failed: ' . $e->getMessage()); + return null; + } } - } } diff --git a/app/Imports/EventsImport.php b/app/Imports/EventsImport.php index a1c315d08..80dd3f7c2 100644 --- a/app/Imports/EventsImport.php +++ b/app/Imports/EventsImport.php @@ -9,9 +9,8 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; -class EventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class EventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -56,12 +55,50 @@ public function model(array $row): ?Model 'longitude' => $row['longitude'], 'latitude' => $row['latitude'], 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } return $event; diff --git a/app/Imports/GenericEventsImport.php b/app/Imports/GenericEventsImport.php index f5e86c8a8..ef6cca6e4 100644 --- a/app/Imports/GenericEventsImport.php +++ b/app/Imports/GenericEventsImport.php @@ -10,10 +10,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class GenericEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class GenericEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -64,6 +63,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -74,9 +108,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/HamburgEventsImport.php b/app/Imports/HamburgEventsImport.php index 2ef82da0c..905781898 100644 --- a/app/Imports/HamburgEventsImport.php +++ b/app/Imports/HamburgEventsImport.php @@ -9,10 +9,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class HamburgEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class HamburgEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -49,6 +48,41 @@ public function model(array $row): ?Model 'longitude' => $row['longitude'], 'latitude' => $row['latitude'], 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -57,7 +91,10 @@ public function model(array $row): ?Model $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } if ($row['tags']) { diff --git a/app/Imports/IrelandDreamSpaceImport.php b/app/Imports/IrelandDreamSpaceImport.php index 1dd8ebe24..adda2e679 100644 --- a/app/Imports/IrelandDreamSpaceImport.php +++ b/app/Imports/IrelandDreamSpaceImport.php @@ -5,14 +5,12 @@ use App\Event; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Log; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class IrelandDreamSpaceImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class IrelandDreamSpaceImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -61,6 +59,41 @@ public function model(array $row): ?Model 'language' => '', 'approved_by' => 19588, 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); diff --git a/app/Imports/IrelandEventsImport.php b/app/Imports/IrelandEventsImport.php index 5cb2b36ef..676bafa03 100644 --- a/app/Imports/IrelandEventsImport.php +++ b/app/Imports/IrelandEventsImport.php @@ -4,17 +4,15 @@ use App\Event; use App\User; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class IrelandEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class IrelandEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { // public function parseDate($date, $time) { // $time = Date::excelToDateTimeObject($time); @@ -72,6 +70,41 @@ public function model(array $row): ?Model 'latitude' => str_replace(',', '.', $row['latitude']), 'language' => 'en', 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -80,7 +113,10 @@ public function model(array $row): ?Model $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/LuxembourgEventsImport.php b/app/Imports/LuxembourgEventsImport.php index faaf2d34e..ce38073f1 100644 --- a/app/Imports/LuxembourgEventsImport.php +++ b/app/Imports/LuxembourgEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class LuxembourgEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class LuxembourgEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -47,6 +46,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -57,9 +91,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } return $event; diff --git a/app/Imports/MagentaEventsImport.php b/app/Imports/MagentaEventsImport.php index 256d0ac59..2d888b14a 100644 --- a/app/Imports/MagentaEventsImport.php +++ b/app/Imports/MagentaEventsImport.php @@ -7,10 +7,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class MagentaEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class MagentaEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -46,6 +45,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -54,7 +88,10 @@ public function model(array $row): ?Model $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } return $event; diff --git a/app/Imports/ReportedEventsImport.php b/app/Imports/ReportedEventsImport.php index ee0b29534..28956f97b 100644 --- a/app/Imports/ReportedEventsImport.php +++ b/app/Imports/ReportedEventsImport.php @@ -10,10 +10,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class ReportedEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class ReportedEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -67,6 +66,41 @@ public function model(array $row): ?Model 'average_participant_age' => 10, 'percentage_of_females' => 50, 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -77,9 +111,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/TelerikEventsImport.php b/app/Imports/TelerikEventsImport.php index 58b3bb9ba..c1f19c2cc 100644 --- a/app/Imports/TelerikEventsImport.php +++ b/app/Imports/TelerikEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class TelerikEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class TelerikEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -48,12 +47,50 @@ public function model(array $row): ?Model 'longitude' => $row['longitude'], 'latitude' => $row['latitude'], 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); $event->audiences()->attach(explode(',', $row['audience_comma_separated_ids'])); - $event->themes()->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } return $event; diff --git a/app/Imports/UKDigitAllCharityEventsImport.php b/app/Imports/UKDigitAllCharityEventsImport.php index cfae5b1c8..4ce6b698f 100644 --- a/app/Imports/UKDigitAllCharityEventsImport.php +++ b/app/Imports/UKDigitAllCharityEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class UKDigitAllCharityEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class UKDigitAllCharityEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -48,6 +47,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -58,9 +92,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/Imports/UKDigitAllEventsImport.php b/app/Imports/UKDigitAllEventsImport.php index 4811180ca..da6538afa 100644 --- a/app/Imports/UKDigitAllEventsImport.php +++ b/app/Imports/UKDigitAllEventsImport.php @@ -8,10 +8,9 @@ use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithHeadingRow; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Shared\Date; -class UKDigitAllEventsImport extends DefaultValueBinder implements ToModel, WithCustomValueBinder, WithHeadingRow +class UKDigitAllEventsImport extends BaseEventsImport implements ToModel, WithCustomValueBinder, WithHeadingRow { public function parseDate($date) { @@ -46,6 +45,41 @@ public function model(array $row): ?Model 'latitude' => $row['latitude'], 'language' => strtolower($row['language']), 'mass_added_for' => 'Excel', + 'recurring_event' => isset($row['recurring_event']) + ? $this->validateSingleChoice($row['recurring_event'], Event::RECURRING_EVENTS) + : null, + + 'males_count' => isset($row['males_count']) ? (int) $row['males_count'] : null, + 'females_count' => isset($row['females_count']) ? (int) $row['females_count'] : null, + 'other_count' => isset($row['other_count']) ? (int) $row['other_count'] : null, + + 'is_extracurricular_event' => isset($row['is_extracurricular_event']) + ? $this->parseBool($row['is_extracurricular_event']) + : false, + + 'is_standard_school_curriculum' => isset($row['is_standard_school_curriculum']) + ? $this->parseBool($row['is_standard_school_curriculum']) + : false, + + 'is_use_resource' => isset($row['is_use_resource']) + ? $this->parseBool($row['is_use_resource']) + : false, + + 'activity_format' => isset($row['activity_format']) + ? $this->validateMultiChoice($row['activity_format'], Event::ACTIVITY_FORMATS) + : [], + + 'ages' => isset($row['ages']) + ? $this->validateMultiChoice($row['ages'], Event::AGES) + : [], + + 'duration' => isset($row['duration']) + ? $this->validateSingleChoice($row['duration'], Event::DURATIONS) + : null, + + 'recurring_type' => isset($row['recurring_type']) + ? $this->validateSingleChoice($row['recurring_type'], Event::RECURRING_TYPES) + : null, ]); $event->save(); @@ -56,9 +90,10 @@ public function model(array $row): ?Model ->attach(explode(',', $row['audience_comma_separated_ids'])); } if ($row['theme_comma_separated_ids']) { - $event - ->themes() - ->attach(explode(',', $row['theme_comma_separated_ids'])); + $validThemeIds = $this->validateThemes($row['theme_comma_separated_ids'] ?? ''); + if (count($validThemeIds) > 0 ) { + $event->themes()->attach($validThemeIds); + } } Log::info($event->slug); diff --git a/app/MeetAndCodeRSSItem.php b/app/MeetAndCodeRSSItem.php index bdff5460f..c8ac2afb6 100644 --- a/app/MeetAndCodeRSSItem.php +++ b/app/MeetAndCodeRSSItem.php @@ -60,6 +60,31 @@ */ class MeetAndCodeRSSItem extends Model { + protected $fillable = [ + 'activity_format', + 'duration', + 'recurring_event', + 'recurring_type', + 'males_count', + 'females_count', + 'other_count', + 'is_extracurricular_event', + 'is_standard_school_curriculum', + 'is_use_resource', + 'ages' + ]; + + protected function casts(): array + { + return [ + 'activity_format' => 'array', + 'is_extracurricular_event' => 'boolean', + 'is_standard_school_curriculum' => 'boolean', + 'ages' => 'array', + 'is_use_resource' => 'boolean', + ]; + } + public function getCountryIso() { @@ -73,7 +98,6 @@ public function getCountryIso() default: return Country::where('name', 'like', $this->country)->first()->iso; } - } private function mapOrganisationTypes($organisation_type) @@ -84,7 +108,6 @@ private function mapOrganisationTypes($organisation_type) default: return 'other'; } - } private function mapActivityTypes($activity_type) @@ -97,7 +120,6 @@ private function mapActivityTypes($activity_type) default: return 'other'; } - } public function createEvent($user) @@ -125,9 +147,20 @@ public function createEvent($user) 'end_date' => $this->end_date, 'longitude' => $this->lon, 'latitude' => $this->lat, - 'geoposition' => $this->lat.','.$this->lon, + 'geoposition' => $this->lat . ',' . $this->lon, 'language' => MeetAndCodeHelper::getLanguage($this->link), 'mass_added_for' => 'RSS meet_and_code', + 'activity_format' => is_array($this->activity_format) ? $this->activity_format : [], + 'duration' => $this->duration, + 'recurring_event' => $this->recurring_event, + 'recurring_type' => $this->recurring_type, + 'males_count' => $this->males_count, + 'females_count' => $this->females_count, + 'other_count' => $this->other_count, + 'is_extracurricular_event' => $this->is_extracurricular_event, + 'is_standard_school_curriculum' => $this->is_standard_school_curriculum, + 'ages' => is_array($this->ages) ? $this->ages : [], + 'is_use_resource' => $this->is_use_resource, ]); $event->save(); @@ -137,6 +170,5 @@ public function createEvent($user) $event->themes()->attach(8); return $event; - } } diff --git a/app/Observers/EventObserver.php b/app/Observers/EventObserver.php index 73f71d25b..313993fa1 100644 --- a/app/Observers/EventObserver.php +++ b/app/Observers/EventObserver.php @@ -68,4 +68,19 @@ public function forceDeleted(Event $event): void { // } + + /** + * Handle the Event "saving" event. + * + * @param \App\Models\Event $event + * @return void + */ + public function saving(Event $event) + { + if (isset($event->language)) { + if (is_array($event->language)) { + $event->language = implode(',', array_filter($event->language)); + } + } + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 31366704c..0d22c7105 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -9,9 +9,11 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\View; +use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Lang; class AppServiceProvider extends ServiceProvider { @@ -40,6 +42,7 @@ function ($view) { $view->with('audiences', \App\Audience::all()); $view->with('activity_types', \App\ActivityType::list()); $view->with('countries', \App\Country::translated()); + $view->with('languages', Arr::sort(Lang::get('base.languages'))); $view->with('active_countries', \App\Country::withEvents()); $view->with( 'themes', diff --git a/app/Queries/EventsQuery.php b/app/Queries/EventsQuery.php index 1894ae1d1..e1419d13a 100644 --- a/app/Queries/EventsQuery.php +++ b/app/Queries/EventsQuery.php @@ -37,6 +37,13 @@ public static function store(Request $request) abort(503, 'Title error'); } + if (!isset($request['participants_count'])) { + $request['participants_count'] = (int) $request['males_count'] + (int) $request['females_count'] + (int) $request['other_count']; + if ($request['participants_count'] > 0) { + $request['percentage_of_females'] = number_format((int) $request['females_count'] / (int) $request['participants_count'] * 100, 2); + } + } + $request['pub_date'] = Carbon::now(); $request['created'] = Carbon::now(); $request['updated'] = Carbon::now(); @@ -62,6 +69,14 @@ public static function store(Request $request) $request['codeweek_for_all_participation_code'] = $codeweek_4_all_generated_code; } + if (!empty($request['activity_format']) && is_string($request['activity_format'])) { + $request['activity_format'] = explode(',', $request['activity_format']); + } + + if (!empty($request['ages']) && is_string($request['ages'])) { + $request['ages'] = explode(',', $request['ages']); + } + $event = Event::create($request->toArray()); if (! empty($request['tags'])) { @@ -101,11 +116,26 @@ public static function update(Request $request, Event $event) $request['latitude'] = explode(',', $request['geoposition'])[0]; $request['longitude'] = explode(',', $request['geoposition'])[1]; + if (!isset($request['participants_count'])) { + $request['participants_count'] = (int) $request['males_count'] + (int) $request['females_count'] + (int) $request['other_count']; + if ($request['participants_count'] > 0) { + $request['percentage_of_females'] = number_format((int) $request['females_count'] / (int) $request['participants_count'] * 100, 2); + } + } + //In order to appear again in the list for the moderators if ($event->status == 'REJECTED') { $request['status'] = 'PENDING'; } + if (!empty($request['activity_format']) && is_string($request['activity_format'])) { + $request['activity_format'] = explode(',', $request['activity_format']); + } + + if (!empty($request['ages']) && is_string($request['ages'])) { + $request['ages'] = explode(',', $request['ages']); + } + $event->update($request->toArray()); $request['theme'] = explode(',', $request['theme']); diff --git a/app/Queries/SuperOrganiserQuery.php b/app/Queries/SuperOrganiserQuery.php index ac8510d74..f10b52a90 100644 --- a/app/Queries/SuperOrganiserQuery.php +++ b/app/Queries/SuperOrganiserQuery.php @@ -20,7 +20,7 @@ public static function mine() public static function winners($edition) { $winners = DB::table('events') - ->where('status', '=', 'APPROVED') + ->where('status', 'APPROVED') ->whereNull('deleted_at') ->whereYear('end_date', '=', $edition) ->groupBy('creator_id') diff --git a/app/User.php b/app/User.php index 1f45713e1..f33e37b58 100644 --- a/app/User.php +++ b/app/User.php @@ -385,8 +385,8 @@ public function activities($edition) { return DB::table('events') - ->where('creator_id', '=', $this->id) - ->where('status', "=", "APPROVED") + ->where('creator_id', $this->id) + ->where('status', "APPROVED") ->whereNull('deleted_at') ->whereYear('end_date', '=', $edition) ->count(); @@ -397,7 +397,7 @@ public function reported($edition = null) $query = DB::table('events') ->where('creator_id', '=', $this->id) - ->where('status', "=", "APPROVED") + ->where('status', "APPROVED") ->whereNotNull('reported_at') ->whereNull('deleted_at'); @@ -429,7 +429,7 @@ public function influence($edition = null) // Log::info("$nameInTag - $edition not in cache"); $taggedActivities = $this->taggedActivities() - ->where('status', '=', 'APPROVED'); + ->where('status', 'APPROVED'); if (!is_null($edition)) { $taggedActivities->whereYear('events.created_at', '=', $edition); diff --git a/database/migrations/2025_05_06_162855_add_new_fields_for_new_form_to_events_table.php b/database/migrations/2025_05_06_162855_add_new_fields_for_new_form_to_events_table.php new file mode 100644 index 000000000..b7638eae4 --- /dev/null +++ b/database/migrations/2025_05_06_162855_add_new_fields_for_new_form_to_events_table.php @@ -0,0 +1,50 @@ +json('activity_format')->nullable(); + $table->string('duration')->nullable()->after('activity_format'); + $table->string('recurring_event')->nullable()->after('duration'); + $table->string('recurring_type')->nullable()->after('recurring_event'); + $table->integer('males_count')->nullable()->after('recurring_type'); + $table->integer('females_count')->nullable()->after('males_count'); + $table->integer('other_count')->nullable()->after('females_count'); + $table->boolean('is_extracurricular_event')->default(false)->after('other_count'); + $table->boolean('is_standard_school_curriculum')->default(false)->after('is_extracurricular_event'); + $table->json('ages')->nullable()->after('is_standard_school_curriculum'); + $table->boolean('is_use_resource')->default(false)->after('ages'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('events', function (Blueprint $table) { + $table->dropColumn([ + 'activity_format', + 'duration', + 'recurring_event', + 'recurring_type', + 'males_count', + 'females_count', + 'other_count', + 'is_extracurricular_event', + 'is_standard_school_curriculum', + 'ages', + 'is_use_resource', + ]); + }); + } +}; diff --git a/database/migrations/2025_05_15_102937_add_new_fields_for_new_form_to_meet_and_code_r_s_s_items_table.php b/database/migrations/2025_05_15_102937_add_new_fields_for_new_form_to_meet_and_code_r_s_s_items_table.php new file mode 100644 index 000000000..995db3c3c --- /dev/null +++ b/database/migrations/2025_05_15_102937_add_new_fields_for_new_form_to_meet_and_code_r_s_s_items_table.php @@ -0,0 +1,38 @@ +json('activity_format')->nullable(); + $table->string('duration')->nullable()->after('activity_format'); + $table->string('recurring_event')->nullable()->after('duration'); + $table->string('recurring_type')->nullable()->after('recurring_event'); + $table->integer('males_count')->nullable()->after('recurring_type'); + $table->integer('females_count')->nullable()->after('males_count'); + $table->integer('other_count')->nullable()->after('females_count'); + $table->boolean('is_extracurricular_event')->default(false)->after('other_count'); + $table->boolean('is_standard_school_curriculum')->default(false)->after('is_extracurricular_event'); + $table->json('ages')->nullable()->after('is_standard_school_curriculum'); + $table->boolean('is_use_resource')->default(false)->after('ages'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('meet_and_code_r_s_s_items', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2025_05_29_064831_update_langague_to_multiple_in_events_table.php b/database/migrations/2025_05_29_064831_update_langague_to_multiple_in_events_table.php new file mode 100644 index 000000000..0f4a9d190 --- /dev/null +++ b/database/migrations/2025_05_29_064831_update_langague_to_multiple_in_events_table.php @@ -0,0 +1,28 @@ +text('language')->nullable()->change()->comment('Comma-separated language codes (e.g. "en,fr,de")'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('events', function (Blueprint $table) { + $table->string('language', 5)->nullable()->change()->comment(null); + }); + } +}; diff --git a/public/images/activity/banner.png b/public/images/activity/banner.png new file mode 100644 index 000000000..4606ddf0d Binary files /dev/null and b/public/images/activity/banner.png differ diff --git a/public/images/check-white.svg b/public/images/check-white.svg new file mode 100644 index 000000000..2edf088d0 --- /dev/null +++ b/public/images/check-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/close-white.svg b/public/images/close-white.svg new file mode 100644 index 000000000..571edd357 --- /dev/null +++ b/public/images/close-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/icon_error.svg b/public/images/icon_error.svg new file mode 100644 index 000000000..de1cef492 --- /dev/null +++ b/public/images/icon_error.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icon_image.svg b/public/images/icon_image.svg new file mode 100644 index 000000000..cc27b798d --- /dev/null +++ b/public/images/icon_image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/icon_question.svg b/public/images/icon_question.svg new file mode 100644 index 000000000..d3d3d0c7f --- /dev/null +++ b/public/images/icon_question.svg @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/public/images/marker-blue.svg b/public/images/marker-blue.svg new file mode 100644 index 000000000..69337484a --- /dev/null +++ b/public/images/marker-blue.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/marker-orange.svg b/public/images/marker-orange.svg new file mode 100644 index 000000000..a58a8c519 --- /dev/null +++ b/public/images/marker-orange.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/select-arrow.svg b/public/images/select-arrow.svg new file mode 100644 index 000000000..8089fbee9 --- /dev/null +++ b/public/images/select-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/star.svg b/public/images/star.svg new file mode 100644 index 000000000..26db2ca91 --- /dev/null +++ b/public/images/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/js/ext/functions.js b/public/js/ext/functions.js index 90a7c4d1e..8006ab720 100755 --- a/public/js/ext/functions.js +++ b/public/js/ext/functions.js @@ -917,14 +917,6 @@ var SEMICOLON = SEMICOLON || {}; $('#primary-menu.sub-title').children('ul').children('.current').prev().css({ backgroundImage : 'none' }); } - if( SEMICOLON.isMobile.Android() ) { - $( '#primary-menu ul li.sub-menu' ).children('a').on('touchstart', function(e){ - if( !$(this).parent('li.sub-menu').hasClass('sfHover') ) { - e.preventDefault(); - } - }); - } - if( SEMICOLON.isMobile.Windows() ) { $('#primary-menu > ul, #primary-menu > div > ul,.top-links > ul').superfish('destroy').addClass('windows-mobile-menu'); diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 34fa24498..739c68221 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -12,6 +12,7 @@ @import "components/pagination"; @import "components/calendar"; @import "components/table"; +@import "components/tinymce"; //Import pages @import "pages"; @@ -236,4 +237,16 @@ body:not(.new-layout) { } } +.marker-popup-content { + .marker-popup-description { + max-height: 200px; + overflow: auto; + p { + font-size: 14px; + padding: 0; + margin: 4px 0; + } + } +} + @import "pages/cookie"; diff --git a/resources/assets/sass/components/forms.scss b/resources/assets/sass/components/forms.scss index e79471a5e..a5f291000 100644 --- a/resources/assets/sass/components/forms.scss +++ b/resources/assets/sass/components/forms.scss @@ -1,4 +1,18 @@ +.custom-geo-input { + input { + width: 100%; + border: 2px solid #a4b8d9; + border-radius: 24px; + height: 48px; + font-size: 20px; + line-height: 28px; + padding: 0 24px; + color: #20262C; + } +} + .multiselect.multi-select { + &.multiselect--active { .multiselect--values { display: none; @@ -9,6 +23,7 @@ } .multiselect__tags{ + cursor: pointer; border-radius: 24px; min-height: 46px; border: 2px solid #A4B8D9; @@ -25,13 +40,13 @@ height: 100%; } .multiselect__placeholder, - .multiselect__single{ + .multiselect__tags .multiselect__single{ padding: 0; margin: 0; font-size: 16px; font-style: normal; font-weight: 400; - color: #333E48; + color: #20262C; } .multiselect__placeholder { max-width: 100%; @@ -41,6 +56,7 @@ text-overflow: ellipsis; line-height: 18px; } + .multiselect__single { padding-top: 6px; color: #333E48; @@ -54,7 +70,7 @@ height: 8px; width: 8px; border: none; - border-left: 2px solid #5F718A; + border-left: 2px solid #1C4DA1; transform: rotate(45deg) !important; } .multiselect__select:after { @@ -66,7 +82,7 @@ height: 8px; width: 8px; border: none; - border-left: 2px solid #5F718A; + border-left: 2px solid #1C4DA1; transform: rotate(-45deg); } .multiselect__tags-wrap{ @@ -78,6 +94,103 @@ margin: 0; } } + + &.large-text { + .multiselect__placeholder, + .multiselect__single{ + font-size: 20px; + line-height: 24px; + } + .multiselect__placeholder { + line-height: 24px; + } + .multiselect__input { + line-height: 24px; + } + .multiselect__tags{ + padding: 9px 40px 8px 16px; + min-height: 48px; + } + } + + &.new-theme { + .multiselect__tags-wrap { + padding: 0; + transform: translateX(-16px); + } + .multiselect__placeholder { + display: block; + line-height: 32px; + } + .multiselect__single { + line-height: 32px; + } + .multiselect__tags { + border-radius: 24px !important; + padding: 6px 46px 6px 24px; + + .multiselect__input { + font-size: 20px; + font-family: Blinker; + line-height: 32px; + margin: 0; + padding: 0; + &::placeholder { + color: #9ca3af; + } + } + } + + .multiselect__content-wrapper { + border: 2px solid #ADB2B6; + border-radius: 12px; + } + + .multiselect__content-wrapper { + overflow: hidden; + padding: 16px 12px 16px 0; + + .multiselect__content { + min-height: 1px; + max-height: calc(300px - 32px); + overflow: auto; + + &::-webkit-scrollbar { + border-radius: 6px; + width: 12px; + display: block; + } + + &::-webkit-scrollbar-track { + background: #E8EDF6; + border-radius: 8px; + } + + &::-webkit-scrollbar-thumb { + background: #1C4DA1; + border-radius: 6px; + } + } + } + + .multiselect__option { + padding: 9px 16px 9px 24px; + font-size: 20px; + &.multiselect__option--highlight { + background-color: #eee; + color: #20262C; + } + &.multiselect__option--selected { + background-color: transparent; + &:hover { + background-color: #eee; + } + } + &:after { + display: none; + } + } + } } .multiselect .multiselect__tags{ @@ -806,4 +919,177 @@ .share-event-wrapper{ margin-top: 5px; } +} + +.custom-date-picker { + font-family: Blinker; + + .dp__outer_menu_wrap { + z-index: 9999999; + } + .dp__menu { + border: 2px solid #ADB2B6 !important; + border-radius: 12px !important; + padding: 6px 12px 10px 12px !important; + + .dp__arrow_top { + border-width: 2px 2px 0 0 !important; + border-color: #ADB2B6 !important; + } + } + + .dp--header-wrap { + margin-bottom: 8px; + + .dp__month_year_wrap { + justify-content: center; + .dp__btn.dp__month_year_select:first-child { + justify-content: flex-end; + padding: 0 4px; + width: auto; + } + + .dp__btn.dp__month_year_select:last-child { + justify-content: flex-start; + padding: 0 4px; + width: auto; + } + } + } + + .dp__instance_calendar { + .dp--tp-wrap { + .dp__btn { + svg { + stroke: #1C4DA1; + fill: #1C4DA1; + } + } + } + } + + .dp__calendar_header_separator { + background-color: #A4B8D9 !important; + } + + .dp__calendar { + gap: 12px; + + .dp__calendar_header_separator { + margin: 8px 0; + } + + .dp__calendar { + padding-bottom: 16px; + border-bottom: 1px solid #A4B8D9 !important; + } + + .dp__calendar_header { + gap: 12px; + .dp__calendar_header_item { + font-size: 20px; + } + } + + .dp__calendar_row { + gap: 12px; + .dp__calendar_item { + font-size: 20px; + .dp__cell_inner { + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + text-align: center; + padding: 0px; + width: 32px; + height: 32px; + &:hover { + background: #E8EDF6; + } + &.db__active_date { + background: #1C4DA1; + &:hover { + background: #1C4DA1; + } + } + } + } + } + } + + .dp--tp-wrap { + width: 100%; + max-width: 100% !important; + padding: 4px 0; + margin-bottom: 10px; + + > button[aria-label="Open time picker"] { + height: auto; + .dp__icon { + width: 24px; + height: 24px; + border-bottom: 1px solid #1C4DA1; + + } + &::after { + font-family: Blinker; + content: 'Select time'; + font-size: 20px; + line-height: 24px; + font-weight: 600; + padding-left: 8px; + color: #1C4DA1; + border-bottom: 1px solid #1C4DA1; + } + } + } + + .dp__action_row { + .dp__selection_preview { + display: none; + } + .dp__action_buttons { + flex-grow: 1; + margin: 0; + width: 100%; + display: flex; + gap: 10px; + + button { + display: flex; + justify-content: center; + align-items: center; + width: 50%; + text-align: center; + border-radius: 24px; + border-width: 2px; + border-style: solid; + height: 40px; + font-weight: 600; + font-size: 18px; + font-family: Blinker; + transition-duration: 300; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + + &.dp__action_cancel { + color: #1C4DA1; + border-color: #1C4DA1; + &:hover { + background-color: #E8EDF6; + } + } + + &.dp__action_select { + border-color: #F95C22; + background-color: #F95C22; + &:hover { + border-color: #FB9D7A; + background-color: #FB9D7A; + } + } + } + } + } } \ No newline at end of file diff --git a/resources/assets/sass/components/header.scss b/resources/assets/sass/components/header.scss index eedead454..f8aa6d18e 100644 --- a/resources/assets/sass/components/header.scss +++ b/resources/assets/sass/components/header.scss @@ -560,9 +560,17 @@ header .lang-menu .menu-dropdown ul li a{ padding-bottom: 40px; > li { + display: flex; + align-items: center; + gap: 12px; margin-top: 24px; padding: 0; + > svg, > img { + width: 16px; + height: 16px; + } + > a { padding: 0; } diff --git a/resources/assets/sass/components/tinymce.scss b/resources/assets/sass/components/tinymce.scss new file mode 100644 index 000000000..518111cb8 --- /dev/null +++ b/resources/assets/sass/components/tinymce.scss @@ -0,0 +1,27 @@ +.custom-tinymce { + .tox-tinymce { + border: 2px solid #a4b8d9; + border-radius: 24px; + } + + .tox-editor-container { + .tox-menubar { + padding: 0 12px; + } + } + .tox-toolbar-overlord { + .tox-toolbar__primary { + .tox-toolbar__group:first-child { + padding-left: 12px; + } + .tox-toolbar__group:last-child { + padding-right: 12px; + } + } + } + + .tox .tox-statusbar { + height: 24px; + padding: 4px 16px 4px 16px; + } +} diff --git a/resources/excel/sample.xlsx b/resources/excel/sample.xlsx index b62539dbe..a351ec147 100644 Binary files a/resources/excel/sample.xlsx and b/resources/excel/sample.xlsx differ diff --git a/resources/js/app.js b/resources/js/app.js index 53c4e4f73..7ef78419b 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,5 +1,6 @@ import './bootstrap'; import { createApp } from 'vue'; +import ActivityForm from './components/activity-form/index.vue'; import ResourceForm from './components/ResourceForm.vue'; import ResourceCard from './components/ResourceCard.vue'; import ResourcePill from './components/ResourcePill.vue'; @@ -15,6 +16,8 @@ import PictureForm from "./components/PictureForm.vue"; import Flash from "./components/Flash.vue"; import InputTags from "./components/InputTags.vue"; import ReportEvent from "./components/ReportEvent.vue"; +import EventCard from "./components/EventCard.vue"; +import EventDetail from "./components/EventDetail.vue"; import SearchPageComponent from "./components/SearchPageComponent.vue"; // import { createI18n } from 'vue-i18n'; @@ -34,6 +37,7 @@ app.use(i18nVue, { return await langs[`../lang/${lang}.json`](); } }) +app.component('ActivityForm', ActivityForm); app.component('ResourceForm', ResourceForm); app.component('ResourceCard', ResourceCard); app.component('ResourcePill', ResourcePill); @@ -52,4 +56,6 @@ app.component('InputTags', InputTags); app.component('SearchPageComponent', SearchPageComponent); app.component('AvatarForm', AvatarForm); app.component('PartnerGallery', PartnerGallery); +app.component('EventCard', EventCard); +app.component('EventDetail', EventDetail); app.mount("#app"); diff --git a/resources/js/components/AutocompleteGeo.vue b/resources/js/components/AutocompleteGeo.vue index 38a9562e8..f3619d443 100644 --- a/resources/js/components/AutocompleteGeo.vue +++ b/resources/js/components/AutocompleteGeo.vue @@ -37,21 +37,26 @@ export default { geoposition: String, location: String }, - setup(props) { + emits: ['onChange'], + setup(props, { emit }) { const item = ref(props.value ? { name: props.value } : null); const items = ref(null); const template = ItemTemplate; - const inputAttrs = { + const inputAttrs = ref({ placeholder: props.placeholder, name: props.name, autocomplete: "off" - }; + }); const localGeoposition = ref(props.geoposition); const initialLocation = props.location; - + watch(() => props.placeholder, () => { + inputAttrs.value.placeholder = props.placeholder; + }); const itemSelected = (selectedItem) => { + emit('onChange', { location: selectedItem?.name || '' }); + if (selectedItem && selectedItem.name && selectedItem.magicKey) { const baseURL = "/api/proxy/geocode"; // Update to your Laravel endpoint axios.get(baseURL, { @@ -66,7 +71,16 @@ export default { window.map.setView([candidate.location.y, candidate.location.x], 16); } const countryIso2 = findCountry(candidate.attributes.Country).iso2; - document.getElementById('id_country').value = countryIso2; + + emit('onChange', { + location: selectedItem?.name || '', + geoposition: [candidate.location.y, candidate.location.x], + country_iso: countryIso2 || '', + }); + + if (document.getElementById('id_country')) { + document.getElementById('id_country').value = countryIso2; + } }).catch(error => { console.error('Error:', error); }); diff --git a/resources/js/components/DateTime.vue b/resources/js/components/DateTime.vue index f5a624413..982b76cbd 100644 --- a/resources/js/components/DateTime.vue +++ b/resources/js/components/DateTime.vue @@ -1,7 +1,7 @@ @@ -11,7 +11,7 @@ import '@vuepic/vue-datepicker/dist/main.css' export default { components: { VueDatePicker }, - props: ['name','placeholder','value','lang'], + props: ['name','placeholder','value','lang', 'format', 'onClear', 'flow'], data() { return { time1: this.value ? this.value : '', @@ -24,6 +24,21 @@ export default { } ] } + }, + methods: { + onChange(date) { + this.$emit('onClear'); + if (!(date instanceof Date) || isNaN(date.getTime())) return ''; + const pad = (n) => n.toString().padStart(2, '0'); + + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + const hour = pad(date.getHours()); + const minute = pad(date.getMinutes()); + + this.$emit('onChange', `${year}-${month}-${day} ${hour}:${minute}`); + } } } @@ -32,12 +47,13 @@ export default { :deep(.dp__input) { border: none; --tw-ring-color: none; - color: #00000080; - font-family: "PT Sans"; - font-size: 16px; + color: #20262C; + font-family: "Blinker"; + font-size: 20px; font-style: normal; font-weight: 400; line-height: 24px; + background-color: transparent; } :deep(.dp__icon) { @@ -47,7 +63,7 @@ export default { :deep(.dp__menu_inner) { color: #00000080; - font-family: "PT Sans"; + font-family: "Blinker"; font-size: 16px; font-style: normal; font-weight: 400; diff --git a/resources/js/components/EventCard.vue b/resources/js/components/EventCard.vue new file mode 100644 index 000000000..adb9f321d --- /dev/null +++ b/resources/js/components/EventCard.vue @@ -0,0 +1,153 @@ + + + diff --git a/resources/js/components/EventDetail.vue b/resources/js/components/EventDetail.vue new file mode 100644 index 000000000..6ed3523fb --- /dev/null +++ b/resources/js/components/EventDetail.vue @@ -0,0 +1,439 @@ + + + diff --git a/resources/js/components/Pagination.vue b/resources/js/components/Pagination.vue index 88e5e7676..83279bdd2 100644 --- a/resources/js/components/Pagination.vue +++ b/resources/js/components/Pagination.vue @@ -16,7 +16,7 @@
  • {{ page }} diff --git a/resources/js/components/SearchPageComponent.vue b/resources/js/components/SearchPageComponent.vue index 15afe7a7a..c4a3f6420 100644 --- a/resources/js/components/SearchPageComponent.vue +++ b/resources/js/components/SearchPageComponent.vue @@ -1,285 +1,688 @@ diff --git a/resources/js/components/Tooltip.vue b/resources/js/components/Tooltip.vue new file mode 100644 index 000000000..56757486b --- /dev/null +++ b/resources/js/components/Tooltip.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/resources/js/components/activity-form/AddConfirmation.vue b/resources/js/components/activity-form/AddConfirmation.vue new file mode 100644 index 000000000..ca0a58874 --- /dev/null +++ b/resources/js/components/activity-form/AddConfirmation.vue @@ -0,0 +1,270 @@ + + + diff --git a/resources/js/components/activity-form/FormStep1.vue b/resources/js/components/activity-form/FormStep1.vue new file mode 100644 index 000000000..1989d03a1 --- /dev/null +++ b/resources/js/components/activity-form/FormStep1.vue @@ -0,0 +1,265 @@ + + + diff --git a/resources/js/components/activity-form/FormStep2.vue b/resources/js/components/activity-form/FormStep2.vue new file mode 100644 index 000000000..59b0e03c3 --- /dev/null +++ b/resources/js/components/activity-form/FormStep2.vue @@ -0,0 +1,240 @@ + + + diff --git a/resources/js/components/activity-form/FormStep3.vue b/resources/js/components/activity-form/FormStep3.vue new file mode 100644 index 000000000..515bf5d3e --- /dev/null +++ b/resources/js/components/activity-form/FormStep3.vue @@ -0,0 +1,184 @@ + + + diff --git a/resources/js/components/activity-form/index.vue b/resources/js/components/activity-form/index.vue new file mode 100644 index 000000000..9da57f73c --- /dev/null +++ b/resources/js/components/activity-form/index.vue @@ -0,0 +1,599 @@ + + + diff --git a/resources/js/components/activity-form/mixins.js b/resources/js/components/activity-form/mixins.js new file mode 100644 index 000000000..1d10963ac --- /dev/null +++ b/resources/js/components/activity-form/mixins.js @@ -0,0 +1,135 @@ +import { computed } from 'vue'; +import { trans } from 'laravel-vue-i18n'; + +export function useDataOptions() { + const buildOptionMap = (options) => { + const result = {}; + options?.forEach((option) => { + result[option.id] = option.name; + }); + return result; + }; + + const stepTitles = computed(() => [ + 'Activity overview', + 'Who is the activity for', + 'Organiser', + ]); + + const activityFormatOptions = computed(() => [ + { id: 'coding-camp', name: 'Coding camp' }, + { id: 'summer-camp', name: 'Summercamp' }, + { id: 'weekend-course', name: 'Weekend course' }, + { id: 'evening-course', name: 'Evening course' }, + { id: 'careerday', name: 'Careerday' }, + { id: 'university-visit', name: 'University visit' }, + { id: 'coding-home', name: 'Coding@Home' }, + { id: 'code-week-challenge', name: 'Code Week Challenge' }, + { id: 'competition', name: 'Competition' }, + { id: 'other', name: 'Other (e.g. Group work, Seminars, Workshops' }, + ]); + const activityFormatOptionsMap = computed(() => + buildOptionMap(activityFormatOptions.value) + ); + + const activityTypeOptions = computed(() => [ + { + id: 'open-online', + name: trans('event.activitytype.open-online'), + }, + { + id: 'invite-online', + name: trans('event.activitytype.invite-online'), + }, + { + id: 'open-in-person', + name: trans('event.activitytype.open-in-person'), + }, + { + id: 'invite-in-person', + name: trans('event.activitytype.invite-in-person'), + }, + { + id: 'other', + name: trans('event.organizertype.other'), + }, + ]); + const activityTypeOptionsMap = computed(() => + buildOptionMap(activityTypeOptions.value) + ); + + const recurringFrequentlyMap = computed(() => ({ + daily: 'Daily', + weekly: 'Weekly', + monthly: 'Monthly', + })); + + const durationOptions = computed(() => [ + { id: '0-1', name: trans('event.duration.0-1-hour') }, + { id: '1-2', name: trans('event.duration.1-2-hours') }, + { id: '2-4', name: trans('event.duration.2-4-hours') }, + { + id: 'over-4', + name: trans('event.duration.more-than-4-hours'), + }, + ]); + const durationOptionsMap = computed(() => + buildOptionMap(durationOptions.value) + ); + + const recurringTypeOptions = computed(() => [ + { + id: 'consecutive', + name: 'Consecutive learning over multiple sessions', + }, + { + id: 'individual', + name: 'Individual, stand-alone lessons under a common theme/joint event.', + }, + ]); + const recurringTypeOptionsMap = computed(() => + buildOptionMap(recurringTypeOptions.value) + ); + + const ageOptions = computed(() => [ + { id: 'under-5', name: 'Under 5 - Early learners' }, + { id: '6-9', name: '6-9 - Primary' }, + { id: '10-12', name: '10-12 - Upper primary' }, + { id: '13-15', name: '13-15 - Lower secondary' }, + { id: '16-18', name: '16-18 - Upper secondary' }, + { id: '19-25', name: '19-25 - Young Adults' }, + { id: 'over-25', name: 'Over 25 - Adults' }, + ]); + const ageOptionsMap = computed(() => buildOptionMap(ageOptions.value)); + + const organizerTypeOptions = computed(() => [ + { id: 'school', name: trans('event.organizertype.school') }, + { id: 'library', name: trans('event.organizertype.library') }, + { id: 'non profit', name: trans('event.organizertype.non profit') }, + { + id: 'private business', + name: trans('event.organizertype.private business'), + }, + { id: 'other', name: trans('event.organizertype.other') }, + ]); + const organizerTypeOptionsMap = computed(() => + buildOptionMap(organizerTypeOptions.value) + ); + + return { + stepTitles, + activityFormatOptions, + activityFormatOptionsMap, + activityTypeOptions, + activityTypeOptionsMap, + recurringFrequentlyMap, + durationOptions, + durationOptionsMap, + recurringTypeOptions, + recurringTypeOptionsMap, + ageOptions, + ageOptionsMap, + organizerTypeOptions, + organizerTypeOptionsMap, + }; +} diff --git a/resources/js/components/form-fields/CheckboxField.vue b/resources/js/components/form-fields/CheckboxField.vue new file mode 100644 index 000000000..6c587757c --- /dev/null +++ b/resources/js/components/form-fields/CheckboxField.vue @@ -0,0 +1,55 @@ + + + diff --git a/resources/js/components/form-fields/FieldWrapper.vue b/resources/js/components/form-fields/FieldWrapper.vue new file mode 100644 index 000000000..46bc0dcbf --- /dev/null +++ b/resources/js/components/form-fields/FieldWrapper.vue @@ -0,0 +1,85 @@ + + + diff --git a/resources/js/components/form-fields/ImageField.vue b/resources/js/components/form-fields/ImageField.vue new file mode 100644 index 000000000..eb26d484e --- /dev/null +++ b/resources/js/components/form-fields/ImageField.vue @@ -0,0 +1,168 @@ + + + diff --git a/resources/js/components/form-fields/InputField.vue b/resources/js/components/form-fields/InputField.vue new file mode 100644 index 000000000..b012ae8ae --- /dev/null +++ b/resources/js/components/form-fields/InputField.vue @@ -0,0 +1,65 @@ + + + diff --git a/resources/js/components/form-fields/RadioField.vue b/resources/js/components/form-fields/RadioField.vue new file mode 100644 index 000000000..695dafae6 --- /dev/null +++ b/resources/js/components/form-fields/RadioField.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/js/components/form-fields/SelectField.vue b/resources/js/components/form-fields/SelectField.vue new file mode 100644 index 000000000..9cf5e418c --- /dev/null +++ b/resources/js/components/form-fields/SelectField.vue @@ -0,0 +1,210 @@ + + + diff --git a/resources/js/components/form-fields/TinymceField.vue b/resources/js/components/form-fields/TinymceField.vue new file mode 100644 index 000000000..63d50a217 --- /dev/null +++ b/resources/js/components/form-fields/TinymceField.vue @@ -0,0 +1,81 @@ +