diff --git a/.travis.yml b/.travis.yml index e4c81e4c1d..01e66aeffd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,3 +38,4 @@ script: - sed -e "/idle_in_transaction_session_timeout/d" -e 's/ IMMUTABLE / /' -e "s/AS '.*libpgosm.*',/AS 'libpgosm',/" -e "/^--/d" db/structure.sql > db/structure.actual - diff -uw db/structure.expected db/structure.actual - bundle exec rake test:db + - bundle exec rake cucumber diff --git a/Design.md b/Design.md new file mode 100644 index 0000000000..9f7ef32090 --- /dev/null +++ b/Design.md @@ -0,0 +1,193 @@ +# Microcosm + +Micrososm is a website that supports the activities of OpenStreetMap local user groups. These activities include: + +* membership tracking +* communication with members +* showcase recent achievements of the microcosm +* inform people about upcoming mapping events +* highlight places of staleness on the local map +* review changsets +* build walkable and bikable routes for fixing OSM bugs + +## Sample URLs + +There are various hosting options: + +> http://mappingdc.org/microcosm + +> http://openstreetmap.us/microcosms/mappingdc + +> http://openstreetmap.org/microcosms/paris + +# Users + +* Visitor - Someone who is interested in mapping, but not a member of a microcosm. +* Member - Primary user is a mapper who belongs to the microcosm. The mapper goes to events, does street level mapping, edits the map in this area. +* Organizer - Secondary user is the organizers/team of the microcosm. +* Administrator - OSM admin may create microcosms. + +# Features + +## As a Visitor + +### About the map + +- [ ] See Notes on the map that need resolution + +### About the microcosm + +- [x] See a list of microcosms +- [x] See description of the microcosm +- [x] See the members of the microcosm +- [x] See links to facebook and twitter +- [ ] See a map of the area +- [ ] See links to members OSM wiki, OSM help, OSM Forum, GitHub, Mapillary, HOT OSM, and Twitter accounts if they exist. +- [ ] See what the microcosm is working on +- [ ] See what's new in the microcosm: editing activity, project activity +- [ ] See feed from twitter +- [ ] Not be invited more than once per year + +### About the microcosm events + +- [x] See upcoming events +- [ ] See past events + +### About the microcosm projects + +- [ ] See the projects of the microcosm + +## As a Member + +### About the microcosm + +- [ ] See details about other members +- [ ] See mayors of neighborhoods + +### About the member + +- [ ] See their profile +- [ ] See their upcoming events +- [ ] See their past events +- [ ] See their progress +- [ ] See where they have mapped +- [ ] Share that they belong to a Local Chapter +- [ ] Add friends (use OSM profile friends) + +### About events + +- [ ] Propose a new event +- [x] RSVP for an event + +### About projects + +- [ ] Propose a new project +- [ ] Elect to work on a project +- [ ] Work on a mapping task (task manager, local project) + +### Other + +## As an Organizer + +### About the mapathon + +- [ ] Can adjust the center location and bounds of the AOI + +### About the microcosm + +- [ ] Manage the description +- [ ] Identify sister microcosms + +### Events + +- [ ] Organize an event +- [ ] Generate Field Papers (Survey Papers) + +### Membership + +- [ ] Manage members +- [ ] Send a message to members +- [ ] Get notified about first time mappers in the area (https://github.com/cliffordsnow/newUsers) +- [ ] Invite people to join the microcosm + +### Quality Assurance + +- [ ] Measure the completeness of coverage +- [ ] Organize feeds (e.g. city bike station locations) +- [ ] Measure quality assurance + +## As an admin + +- [ ] Create microcosms +- [x] Edit microcosms +- [ ] Periodically scan the wiki for new user groups and local chapters (https://github.com/osmlab/localgroups/blob/master/osmgroups.geojson) + +# QA + +* https://wiki.openstreetmap.org/wiki/Keep_Right +* https://www.keepright.at/report_map.php?zoom=12&lat=39.95356&lon=-75.12364 +* https://github.com/keepright/keepright +* http://osmose.openstreetmap.fr/en/map/ +* https://wiki.openstreetmap.org/wiki/OSM_Inspector + + +# Use Cases + +## List reviews in the area + +Some people want their changes reviewed. Provide a list of these for the AOI. + +## An organizer organizes a street survey + +Assume: microcosm exists and has many users, event details have been selected + +Steps: + +1. Organizer notifies the microcosm about the event. +1. Members RSVP. +1. The event is held. + +## At an event members upload pictures of their survey notes + +At an event there may not be time for surveyors to enter all their data. They can take pictures of their notes and upload it to the microcosm for other people to map later. The notes are entered into a queue of tasks for others to assign to themselves. + +Build native apps for iOS and Android to use the camera and upload them to the server. + +## Build-a-mapathon + +* Help an organizer pick a location to map based on various criteria like location of development, staleness, feasability (mass transit), etc. +* Find a quiet place to sit and edit. +* Break down area to be surveyed into walkable pieces for teams. +* Print Field Papers. + +## Map Fixing for Individuals + +* Find map bugs and generate a bikable or walkable path to cover these points. +* It should be a max bang for your buck type of optimization (use pgrouting). +* Incorporate mobile apps like StreetComplete and OSMBugs. + +# Use + +* Feature flags - pda/flip, fetlife/rollout +* Internationalization + +# Ideas + +* nanocosm +* Map of user groups around the world http://usergroups.openstreetmap.de/ +* DC Wiki - How do we do x? e.g. sidewalks + +# See Also + +* https://wiki.openstreetmap.org/wiki/User_group +* https://github.com/maptime/maptime.github.io/blob/master/_data/chapters.json +* https://wiki.openstreetmap.org/wiki/User:Mvexel/New_User_Welcome_Message +* https://www.wmata.com/schedules/timetables/all-routes.cfm?State=DC +* https://github.com/fossgis/usergroups-bot - This is a bot written in Python, collection all Template:User_group together and generating a KML file to show them on a map: http://usergroups.openstreetmap.de + +# Integration + +* https://github.com/kort/kort +* StreetComplete + + diff --git a/Gemfile b/Gemfile index c2bb066b7c..4a5bff8da4 100644 --- a/Gemfile +++ b/Gemfile @@ -131,6 +131,9 @@ gem "aws-sdk-s3" # Used to resize user images gem "mini_magick" +# Used to provide clean urls like /microcosm/mappingdc +gem "friendly_id" + # Gems useful for development group :development do gem "annotate" @@ -142,6 +145,9 @@ end # Gems needed for running tests group :test do + gem "cucumber-rails", :require => false + # database_cleaner is not required, but highly recommended + gem "database_cleaner" gem "fakefs", :require => "fakefs/safe" gem "minitest", "~> 5.1", :platforms => [:ruby_19, :ruby_20] gem "rails-controller-testing" diff --git a/Gemfile.lock b/Gemfile.lock index 9e1e32109f..aeea71908c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,6 +76,7 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.1.0) aws-eventstream (~> 1.0, >= 1.0.2) + backports (3.15.0) better_errors (2.5.1) coderay (>= 1.0.0) erubi (>= 1.0.0) @@ -132,7 +133,30 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) + cucumber (3.1.2) + builder (>= 2.1.2) + cucumber-core (~> 3.2.0) + cucumber-expressions (~> 6.0.1) + cucumber-wire (~> 0.0.1) + diff-lcs (~> 1.3) + gherkin (~> 5.1.0) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.2) + cucumber-core (3.2.1) + backports (>= 3.8.0) + cucumber-tag_expressions (~> 1.1.0) + gherkin (~> 5.0) + cucumber-expressions (6.0.1) + cucumber-rails (1.8.0) + capybara (>= 2.12, < 4) + cucumber (>= 3.0.2, < 4) + mime-types (>= 2.0, < 4) + nokogiri (~> 1.8) + railties (>= 4.2, < 7) + cucumber-tag_expressions (1.1.1) + cucumber-wire (0.0.1) dalli (2.7.10) + database_cleaner (1.7.0) debug_inspector (0.0.3) deep_merge (1.2.1) delayed_job (4.1.8) @@ -140,6 +164,7 @@ GEM delayed_job_active_record (4.1.4) activerecord (>= 3.0, < 6.1) delayed_job (>= 3.0, < 5) + diff-lcs (1.3) docile (1.3.2) dry-configurable (0.8.3) concurrent-ruby (~> 1.0) @@ -193,10 +218,13 @@ GEM ffi (1.11.1) ffi-libarchive (0.4.10) ffi (~> 1.0) + friendly_id (5.2.5) + activerecord (>= 4.0.0) fspath (3.1.2) gd2-ffij (0.4.0) ffi (>= 1.0.0) geoip (1.6.4) + gherkin (5.1.0) globalid (0.4.2) activesupport (>= 4.2.0) hashdiff (1.0.0) @@ -252,6 +280,9 @@ GEM marcel (0.3.3) mimemagic (~> 0.3.2) method_source (0.9.2) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2019.0331) mimemagic (0.3.3) mini_magick (4.9.5) mini_mime (1.0.2) @@ -259,6 +290,7 @@ GEM minitest (5.12.2) msgpack (1.3.1) multi_json (1.13.1) + multi_test (0.1.2) multi_xml (0.6.0) multipart-post (2.1.1) nio4r (2.5.2) @@ -463,7 +495,9 @@ DEPENDENCIES composite_primary_keys (~> 11.1.0) config coveralls + cucumber-rails dalli + database_cleaner delayed_job_active_record dynamic_form erb_lint @@ -471,6 +505,7 @@ DEPENDENCIES fakefs faraday ffi-libarchive + friendly_id gd2-ffij (>= 0.4.0) geoip htmlentities @@ -524,4 +559,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.17.2 + 1.17.3 diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index c34f357a97..397efb2583 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -28,6 +28,8 @@ def initialize(user) can [:history, :version], OldNode can [:history, :version], OldWay can [:history, :version], OldRelation + can [:index, :show, :show_events, :show_members], Microcosm + can [:show, :index], Event end if user @@ -42,6 +44,11 @@ def initialize(user) can [:new, :create], Report can [:mine, :new, :create, :edit, :update, :delete], Trace can [:account, :go_public, :make_friend, :remove_friend], User + can [:create, :update], EventAttendance + can [:edit, :update], Microcosm, :microcosm_members => { :user => { :id => user.id }, :role => MicrocosmMember::Roles::ORGANIZER } + can [:create], MicrocosmMember + can [:edit, :update], MicrocosmMember, :microcosm => { :microcosm_members => { :user => { :id => user.id }, :role => MicrocosmMember::Roles::ORGANIZER } } + can [:new, :create], Event, :microcosm => { :microcosm_members => { :user => { :id => user.id }, :role => MicrocosmMember::Roles::ORGANIZER } } if user.moderator? can [:hide, :hidecomment], DiaryEntry @@ -57,6 +64,8 @@ def initialize(user) can :create, IssueComment can [:set_status, :delete, :index], User can [:grant, :revoke], UserRole + can [:new, :create, :update], Microcosm + can [:edit, :update], MicrocosmMember end end end diff --git a/app/assets/images/dc_library.png b/app/assets/images/dc_library.png new file mode 100644 index 0000000000..d65d054b39 Binary files /dev/null and b/app/assets/images/dc_library.png differ diff --git a/app/assets/images/microcosm_dc.png b/app/assets/images/microcosm_dc.png new file mode 100644 index 0000000000..0f071dbbf8 Binary files /dev/null and b/app/assets/images/microcosm_dc.png differ diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 40c5eaa2a8..b7e43ab978 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -2915,3 +2915,10 @@ input.richtext_title[type="text"] { } } } + +.flex_row { + display: flex; + div { + padding: 10px; + } +} diff --git a/app/assets/stylesheets/event_attendances.scss b/app/assets/stylesheets/event_attendances.scss new file mode 100644 index 0000000000..e20671d7aa --- /dev/null +++ b/app/assets/stylesheets/event_attendances.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the EventAttendances controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/events.scss b/app/assets/stylesheets/events.scss new file mode 100644 index 0000000000..524da1b808 --- /dev/null +++ b/app/assets/stylesheets/events.scss @@ -0,0 +1,23 @@ +// Place all the styles related to the Events controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ + +.event_details { + .when { + color: #777; font-size: 120%; + } +} + +.event_attendees > a { + display: block; + width: 8em; + height: 8em; + border: 1px solid #888; + border-radius: 4px; + float: left; + margin: 1em; + padding: 1em; + background-color: rgb(238, 238, 238); + box-sizing: border-box; + text-decoration: none; +} diff --git a/app/assets/stylesheets/microcosm_member.scss b/app/assets/stylesheets/microcosm_member.scss new file mode 100644 index 0000000000..7d36fe2347 --- /dev/null +++ b/app/assets/stylesheets/microcosm_member.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the MicrocosmMember controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/assets/stylesheets/microcosms.scss b/app/assets/stylesheets/microcosms.scss new file mode 100644 index 0000000000..bad348ebc6 --- /dev/null +++ b/app/assets/stylesheets/microcosms.scss @@ -0,0 +1,32 @@ +// Place all the styles related to the Microcosms controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ + +.mic_details { + h1 { + margin-top: 0; + } + label { + font-weight: bold; + } + ul { + display: inline-block; + } + ul > li { + display: inline-block; + } +} + +.mic_members > a { + display: block; + width: 8em; + height: 8em; + border: 1px solid #888; + border-radius: 4px; + float: left; + margin: 1em; + padding: 1em; + background-color: rgb(238, 238, 238); + box-sizing: border-box; + text-decoration: none; +} diff --git a/app/controllers/event_attendances_controller.rb b/app/controllers/event_attendances_controller.rb new file mode 100644 index 0000000000..1662d78cd1 --- /dev/null +++ b/app/controllers/event_attendances_controller.rb @@ -0,0 +1,50 @@ +class EventAttendancesController < ApplicationController + layout "site" + before_action :authorize_web + before_action :set_event_attendance, :only => [:update] + + authorize_resource + + def create + attendance = EventAttendance.new(attendance_params) + attendance.intention = intention + if attendance.save! + redirect_to event_path(attendance.event), :notice => "Attendance was successfully saved." + else + redirect_to event_path(attendance.event), :notice => "Attendance was not saved." + end + end + + def update + respond_to do |format| + attendance = EventAttendance.find(params[:id]) + attendance.intention = intention + if attendance.update(attendance_params) + format.html { redirect_to @event_attendance.event, :notice => "Attendance was successfully updated." } + else + format.html { render :edit } + end + end + end + + private + + def intention + # Validate the intention. + # TODO: There must be a better way to do this. + if params[:commit] == "Yes" + intention = "Yes" + elsif params[:commit] == "No" + intention = "No" + end + intention + end + + def set_event_attendance + @event_attendance = EventAttendance.find(params[:id]) + end + + def attendance_params + params.require(:event_attendance).permit(:event_id, :user_id) + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb new file mode 100644 index 0000000000..87bb3e2ce7 --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,54 @@ +class EventsController < ApplicationController + layout "site" + before_action :authorize_web + before_action :set_event, :only => [:edit, :show] + + authorize_resource + + # GET /events + # GET /events.json + def index + @events = Event.all + end + + # GET /events/new + def new + @event = Event.new + @event.microcosm_id = params[:microcosm_id] + end + + # POST /events + # POST /events.json + def create + @event = Event.new(event_params) + + respond_to do |format| + if @event.save + format.html { redirect_to @event, :notice => "Event was successfully created." } + format.json { render :show, :status => :created, :location => @event } + else + format.html { render :new } + format.json { render :json => @event.errors, :status => :unprocessable_entity } + end + end + end + + # GET /events/1/edit + def edit; end + + # GET /events/1 + # GET /events/1.json + def show + @my_attendance = EventAttendance.find_or_initialize_by(:event_id => @event.id, :user_id => current_user&.id) + end + + private + + def set_event + @event = Event.find(params[:id]) + end + + def event_params + params.require(:event).permit(:title, :moment, :location, :description, :microcosm_id) + end +end diff --git a/app/controllers/microcosm_member_controller.rb b/app/controllers/microcosm_member_controller.rb new file mode 100644 index 0000000000..1177f6bc4b --- /dev/null +++ b/app/controllers/microcosm_member_controller.rb @@ -0,0 +1,39 @@ +class MicrocosmMemberController < ApplicationController + layout "site" + before_action :authorize_web + authorize_resource + + before_action :set_microcosm_member, :only => [:edit, :update] + + def create + membership = MicrocosmMember.new(mm_params) + membership.role = MicrocosmMember::Roles::MEMBER + if membership.save! + redirect_to microcosm_path(membership.microcosm), :notice => "Member was successfully created." + else + redirect_to microcosm_path(membership.microcosm), :notice => "Member was not saved." + end + end + + def edit; end + + def update + respond_to do |format| + if @microcosm_member.update(mm_params) + format.html { redirect_to @microcosm_member.microcosm, :notice => "Microcosm Member was successfully updated." } + else + format.html { render :edit } + end + end + end + + private + + def set_microcosm_member + @microcosm_member = MicrocosmMember.find(params[:id]) + end + + def mm_params + params.require(:microcosm_member).permit(:microcosm_id, :user_id, :role) + end +end diff --git a/app/controllers/microcosms_controller.rb b/app/controllers/microcosms_controller.rb new file mode 100644 index 0000000000..71dd5f7bd3 --- /dev/null +++ b/app/controllers/microcosms_controller.rb @@ -0,0 +1,59 @@ +class MicrocosmsController < ApplicationController + layout "site" + before_action :authorize_web + + before_action :set_microcosm, :only => [:edit, :show, :show_events, :show_members, :update] + + authorize_resource + + def index + @microcosms = Microcosm.order(:name) + end + + # GET /microcosms/mycity + # GET /microcosms/mycity.json + def show; end + + def show_members; end + + def show_events; end + + def edit; end + + def update + respond_to do |format| + if @microcosm.update(microcosm_params) + format.html { redirect_to @microcosm, :notice => "Microcosm was successfully updated." } + else + format.html { render :edit } + end + end + end + + def new + @microcosm = Microcosm.new + end + + def create + @microcosm = Microcosm.new(microcosm_params) + if @microcosm.save! + redirect_to microcosms_path, :notice => "Member was successfully created." + else + redirect_to microcosms_path, :notice => "Member was not saved." + end + end + + private + + def set_microcosm + @microcosm = Microcosm.friendly.find(params[:id]) + end + + def microcosm_params + params.require(:microcosm).permit( + :name, :location, :lat, :lon, + :min_lat, :max_lat, :min_lon, :max_lon, + :description + ) + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 0000000000..7c844f519d --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: events +# +# id :bigint(8) not null, primary key +# title :string not null +# moment :datetime +# location :string +# description :text +# microcosm_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# + +class Event < ActiveRecord::Base + belongs_to :microcosm + has_many :event_attendances + + def attendees + EventAttendance.where(:event_id => id, :intention => "Yes") + end +end diff --git a/app/models/event_attendance.rb b/app/models/event_attendance.rb new file mode 100644 index 0000000000..00f70ce93b --- /dev/null +++ b/app/models/event_attendance.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: event_attendances +# +# id :bigint(8) not null, primary key +# user_id :integer not null +# event_id :integer not null +# intention :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_event_attendances_on_event_id (event_id) +# index_event_attendances_on_user_id (user_id) +# + +class EventAttendance < ActiveRecord::Base + belongs_to :event + belongs_to :user +end diff --git a/app/models/microcosm.rb b/app/models/microcosm.rb new file mode 100644 index 0000000000..a12dedee40 --- /dev/null +++ b/app/models/microcosm.rb @@ -0,0 +1,43 @@ +# == Schema Information +# +# Table name: microcosms +# +# id :bigint(8) not null, primary key +# name :string not null +# description :text +# created_at :datetime not null +# updated_at :datetime not null +# slug :string not null +# location :string not null +# lat :decimal(, ) not null +# lon :decimal(, ) not null +# min_lat :integer not null +# max_lat :integer not null +# min_lon :integer not null +# max_lon :integer not null +# + +class Microcosm < ActiveRecord::Base + extend FriendlyId + friendly_id :name, :use => :slugged + self.ignored_columns = ["key"] + + has_many :microcosm_members + has_many :users, :through => :microcosm_members # TODO: counter_cache + has_many :microcosm_links + has_many :events + + def set_link(site, url) + link = MicrocosmLink.find_or_initialize_by(:microcosm_id => id, :site => site) + link.url = url + link.save! + end + + def organizer?(user) + microcosm_members.where(:user_id => user.id, :role => MicrocosmMember::Roles::ORGANIZER).count.positive? + end + + def organizers + microcosm_members.where(:role => MicrocosmMember::Roles::ORGANIZER) + end +end diff --git a/app/models/microcosm_link.rb b/app/models/microcosm_link.rb new file mode 100644 index 0000000000..f5de64c6be --- /dev/null +++ b/app/models/microcosm_link.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: microcosm_links +# +# id :bigint(8) not null, primary key +# microcosm_id :integer not null +# site :string not null +# url :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_microcosm_links_on_microcosm_id (microcosm_id) +# + +class MicrocosmLink < ActiveRecord::Base + belongs_to :microcosm +end diff --git a/app/models/microcosm_member.rb b/app/models/microcosm_member.rb new file mode 100644 index 0000000000..270b22d1d0 --- /dev/null +++ b/app/models/microcosm_member.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: microcosm_members +# +# id :bigint(8) not null, primary key +# microcosm_id :integer not null +# user_id :integer not null +# role :string(64) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_microcosm_members_on_microcosm_id (microcosm_id) +# index_microcosm_members_on_microcosm_id_and_user_id_and_role (microcosm_id,user_id,role) UNIQUE +# index_microcosm_members_on_user_id (user_id) +# + +class MicrocosmMember < ActiveRecord::Base + belongs_to :microcosm + belongs_to :user + + # TODO: validate uniqueness of user's role in each microcosm. + + module Roles + ORGANIZER = "organizer".freeze + MEMBER = "member".freeze + ALL_ROLES = [ORGANIZER, MEMBER].freeze + end +end diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb new file mode 100644 index 0000000000..8f4e37b1da --- /dev/null +++ b/app/views/events/_form.html.erb @@ -0,0 +1,46 @@ +<%= form_with(:model => event, :local => true, :html => { :class => "standard-form" }) do |form| %> + <% if event.errors.any? %> +
+

<%= pluralize(event.errors.count, "error") %> prohibited this event from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :title, :class => "standard-label" %> + <%= form.text_field :title, :id => :event_title %> +
+ +
+ <%= form.label :moment, :class => "standard-label" %> + <%= form.datetime_select :moment, :id => :event_moment %> +
+ +
+ <%= form.label :location, :class => "standard-label" %> + <%= form.text_field :location, :id => :event_location %> +
+ +
+ <%= form.label :description, :class => "standard-label" %> + <%= form.text_area :description, :id => :event_description %> +
+ + <% if @event&.microcosm_id %> + <%= form.hidden_field(:microcosm_id, :value => @event.microcosm_id) %> + <% else %> +
+ <%= form.label :microcosm_id, :class => "standard-label" %> + <%= collection_select(:event, :microcosm_id, Microcosm.all, :id, :name, :prompt => true) %> +
+ <% end %> + +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb new file mode 100644 index 0000000000..4a7a3ef5e3 --- /dev/null +++ b/app/views/events/index.html.erb @@ -0,0 +1,32 @@ +

<%= notice %>

+ +

Events

+ + + + + + + + + + + + + + + <% @events.each do |event| %> + + + + + + + + + + + <% end %> + +
TitleMomentLocationDescriptionMicrocosm
<%= event.title %><%= event.moment %><%= event.location %><%= event.description %><%= link_to event.microcosm.name, microcosm_path(event.microcosm) %> + <%= link_to "Show", event %>
diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb new file mode 100644 index 0000000000..94f1313e5b --- /dev/null +++ b/app/views/events/new.html.erb @@ -0,0 +1,3 @@ +

New Event

+ +<%= render "form", :event => @event %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb new file mode 100644 index 0000000000..af53813140 --- /dev/null +++ b/app/views/events/show.html.erb @@ -0,0 +1,50 @@ +
+
+
+ <%= l @event.moment, :format => :friendly %> +
+ +

+ <%= @event.title %> +

+

+ Hosted by <%= link_to @event.microcosm.name, microcosm_path(@event.microcosm) %> +

+

+ Organized by: Andrew Wiseman +

+
+
+

<%= @event.attendees.size %> people are going.

+

Are you going?

+ <%= form_with :model => @my_attendance do |form| %> + <%= form.hidden_field(:event_id, :value => @event.id) %> + <%= form.hidden_field(:user_id, :value => current_user&.id) %> + <%= form.submit :value => "Yes" %> + <%= form.submit :value => "No" %> + <% end %> +
+
+
+
+
+

+ Description: + <%= @event.description %> +

+ Who's going? +

+ <% @event.attendees.each do |attendance| %> + <%= link_to attendance.user.display_name, user_path(attendance.user) %> + <% end %> +

+
+
+

+ Location: <%= @event.location %> +

+ Directions to this location. + <%= image_tag("dc_library.png", :size => "400x400") %> +
+
+<%#= link_to 'Back', events_path %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 725000a136..cc95d12e66 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -48,6 +48,7 @@ <% end -%> <% end %> +
  • <%= link_to t("layouts.microcosms"), microcosms_path %>
  • <%= link_to t("layouts.gps_traces"), traces_path %>
  • <%= link_to t("layouts.user_diaries"), diary_entries_path %>
  • <%= link_to t("layouts.copyright"), copyright_path %>
  • diff --git a/app/views/microcosm_member/_form.html.erb b/app/views/microcosm_member/_form.html.erb new file mode 100644 index 0000000000..8eaf75e6b2 --- /dev/null +++ b/app/views/microcosm_member/_form.html.erb @@ -0,0 +1,20 @@ +<%= form_with(:model => microcosm_member, :local => true) do |form| %> +
    + <%= form.label :microcosm_id %> + <%= collection_select(:microcosm_member, :microcosm_id, Microcosm.all, :id, :name, :prompt => true) %> +
    + +
    + <%= form.label :user_id %> + <%= collection_select(:microcosm_member, :user_id, User.order("display_name").all, :id, :display_name, :prompt => true) %> +
    + +
    + <%= form.label :role %> + <%= form.select(:role, MicrocosmMember::Roles::ALL_ROLES.map { |role| [role.titleize, role] }) %> +
    + +
    + <%= form.submit %> +
    +<% end %> diff --git a/app/views/microcosm_member/edit.html.erb b/app/views/microcosm_member/edit.html.erb new file mode 100644 index 0000000000..555e02980f --- /dev/null +++ b/app/views/microcosm_member/edit.html.erb @@ -0,0 +1,3 @@ +

    Editing Member

    + +<%= render "form", :microcosm_member => @microcosm_member %> diff --git a/app/views/microcosms/_form.html.erb b/app/views/microcosms/_form.html.erb new file mode 100644 index 0000000000..22c9526139 --- /dev/null +++ b/app/views/microcosms/_form.html.erb @@ -0,0 +1,59 @@ +<%= form_for @microcosm do |form| %> +
    + <%= form.label :name %> + <%= form.text_field :name, :id => :microcosm_name %> +
    + +
    + <%= form.label :location %> + <%= form.text_field :location, :id => :microcosm_location %> +
    + +
    + <%= form.label :lat %> + <%= form.text_field :lat, :id => :microcosm_lat %> (decimal) +
    + +
    + <%= form.label :lon %> + <%= form.text_field :lon, :id => :microcosm_lon %> (decimal) +
    + +

    + To get a bounding box use Geofabrik's Tile Calculator tool. +

    + +
    + <%= form.label :min_lat %> + <%= form.text_field :min_lat, :id => :microcosm_min_lat %> (decimal * 10^7) +
    + +
    + <%= form.label :max_lat %> + <%= form.text_field :max_lat, :id => :microcosm_max_lat %> (decimal * 10^7) +
    + +
    + <%= form.label :min_lon %> + <%= form.text_field :min_lon, :id => :microcosm_min_lon %> (decimal * 10^7) +
    + +
    + <%= form.label :max_lon %> + <%= form.text_field :max_lon, :id => :microcosm_max_lon %> (decimal * 10^7) +
    + +
    + <%= form.label :description %> + <%= form.text_area :description, :id => :microcosm_description %> +
    + + + <%#= form.label :welcome_message %> + <%#= form.text_area :welcome_message, :id => :microcosm_welcome_message %> + + +
    + <%= form.submit %> +
    +<% end %> diff --git a/app/views/microcosms/edit.html.erb b/app/views/microcosms/edit.html.erb new file mode 100644 index 0000000000..aaa3646a11 --- /dev/null +++ b/app/views/microcosms/edit.html.erb @@ -0,0 +1,3 @@ +

    Editing Microcosm

    + +<%= render "form", :microcosm => @microcosm %> diff --git a/app/views/microcosms/index.html.erb b/app/views/microcosms/index.html.erb new file mode 100644 index 0000000000..fb46aa2701 --- /dev/null +++ b/app/views/microcosms/index.html.erb @@ -0,0 +1,28 @@ +<% content_for :heading do %> +

    <%= "All Microcosms" %>

    + +<% end %> + + + + + + + + + + + + <% @microcosms.each do |microcosm| %> + + + + + + <% end %> + +
    NameLocation
    <%= microcosm.name %><%= microcosm.location %><%= link_to "Show", microcosm %><% if current_user && microcosm.organizer?(current_user) %> <%= link_to "Edit", edit_microcosm_path(microcosm) %><% end %>
    diff --git a/app/views/microcosms/new.html.erb b/app/views/microcosms/new.html.erb new file mode 100644 index 0000000000..23f37f1285 --- /dev/null +++ b/app/views/microcosms/new.html.erb @@ -0,0 +1,5 @@ +<% content_for :heading do %> +

    <%= "Microcosm" %>

    +<% end %> + +<%= render "form", :microcosm => @microcosm %> diff --git a/app/views/microcosms/show.html.erb b/app/views/microcosms/show.html.erb new file mode 100644 index 0000000000..80d43a1609 --- /dev/null +++ b/app/views/microcosms/show.html.erb @@ -0,0 +1,92 @@ +<% content_for :heading do %> +

    <%= "Microcosm" %>

    +<% end %> + +
    +
    + <%= image_tag("microcosm_dc.png", :size => "400x300") %> +
    +
    +

    + <%= link_to @microcosm.name, @microcosm %> +

    +

    + <%= @microcosm.location %>, <%= @microcosm.lat %>, <%= @microcosm.lon %> +

    +

    + <%= auto_link @microcosm.description %> +

    +
    + +
      + <% @microcosm.microcosm_links.each do |link| %> +
    • + <%= link.site %> +
    • + <% end %> +
    +
    +
    + +
      + <% @microcosm.organizers.each do |membership| %> +
    • + <%= link_to membership.user.display_name, user_path(membership.user) %> +
    • + <% end %> +
    +
    + + <%= form_with :scope => :microcosm_member, :url => microcosm_member_index_path do |form| %> + <%= form.hidden_field(:microcosm_id, :value => @microcosm.id) %> + <%= form.hidden_field(:user_id, :value => current_user&.id) %> + <%= form.submit :value => t("microcosm.join.action") %> + <% end %> + +
    +
    + +
    +
    +

    + <%= link_to "Upcoming Events", events_of_microcosm_path(@microcosm) %> +

    + +

    + Recent Changes +

    + +

    + Diary Entries of Members +

    + +
    +
    +

    + <%= link_to "Members", members_of_microcosm_path(@microcosm) %> +

    +
    + <% @microcosm.microcosm_members.each do |membership| %> + <%= link_to membership.user.display_name, user_path(membership.user) %> + <% end %> +
    +
    +
    diff --git a/app/views/microcosms/show_events.html.erb b/app/views/microcosms/show_events.html.erb new file mode 100644 index 0000000000..8057d388f3 --- /dev/null +++ b/app/views/microcosms/show_events.html.erb @@ -0,0 +1,21 @@ +<% content_for :heading do %> +

    + <%= @microcosm.name + " Events" %> +

    +<% end %> + +

    + Events +

    + +

    + <% if current_user && @microcosm.organizer?(current_user) %> + <%= link_to "new event", new_event_path(:microcosm_id => @microcosm.id) %> + <% end %> +

    diff --git a/app/views/microcosms/show_members.html.erb b/app/views/microcosms/show_members.html.erb new file mode 100644 index 0000000000..edf58c2f7e --- /dev/null +++ b/app/views/microcosms/show_members.html.erb @@ -0,0 +1,23 @@ +<% content_for :heading do %> +

    + <%= @microcosm.name + " Members" %> +

    +<% end %> + +<%#= link_to 'New', new_microcosm_member_path(microcosm_id: @microcosm.id) %> + +

    +Members +

    +
    + +
    diff --git a/config/cucumber.yml b/config/cucumber.yml new file mode 100644 index 0000000000..5aa9c13b42 --- /dev/null +++ b/config/cucumber.yml @@ -0,0 +1,9 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun = rerun.strip.gsub /\s/, ' ' +rerun_opts = rerun.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' diff --git a/config/initializers/friendly_id.rb b/config/initializers/friendly_id.rb new file mode 100644 index 0000000000..2966f4cca0 --- /dev/null +++ b/config/initializers/friendly_id.rb @@ -0,0 +1,107 @@ +# FriendlyId Global Configuration +# +# Use this to set up shared configuration options for your entire application. +# Any of the configuration options shown here can also be applied to single +# models by passing arguments to the `friendly_id` class method or defining +# methods in your model. +# +# To learn more, check out the guide: +# +# http://norman.github.io/friendly_id/file.Guide.html + +FriendlyId.defaults do |config| + # ## Reserved Words + # + # Some words could conflict with Rails's routes when used as slugs, or are + # undesirable to allow as slugs. Edit this list as needed for your app. + config.use :reserved + + config.reserved_words = %w[new edit index session login logout users admin + stylesheets assets javascripts images] + + # This adds an option to treat reserved words as conflicts rather than exceptions. + # When there is no good candidate, a UUID will be appended, matching the existing + # conflict behavior. + + # config.treat_reserved_as_conflict = true + + # ## Friendly Finders + # + # Uncomment this to use friendly finders in all models. By default, if + # you wish to find a record by its friendly id, you must do: + # + # MyModel.friendly.find('foo') + # + # If you uncomment this, you can do: + # + # MyModel.find('foo') + # + # This is significantly more convenient but may not be appropriate for + # all applications, so you must explicity opt-in to this behavior. You can + # always also configure it on a per-model basis if you prefer. + # + # Something else to consider is that using the :finders addon boosts + # performance because it will avoid Rails-internal code that makes runtime + # calls to `Module.extend`. + # + # config.use :finders + # + # ## Slugs + # + # Most applications will use the :slugged module everywhere. If you wish + # to do so, uncomment the following line. + # + # config.use :slugged + # + # By default, FriendlyId's :slugged addon expects the slug column to be named + # 'slug', but you can change it if you wish. + # + # config.slug_column = 'slug' + # + # By default, slug has no size limit, but you can change it if you wish. + # + # config.slug_limit = 255 + # + # When FriendlyId can not generate a unique ID from your base method, it appends + # a UUID, separated by a single dash. You can configure the character used as the + # separator. If you're upgrading from FriendlyId 4, you may wish to replace this + # with two dashes. + # + # config.sequence_separator = '-' + # + # Note that you must use the :slugged addon **prior** to the line which + # configures the sequence separator, or else FriendlyId will raise an undefined + # method error. + # + # ## Tips and Tricks + # + # ### Controlling when slugs are generated + # + # As of FriendlyId 5.0, new slugs are generated only when the slug field is + # nil, but if you're using a column as your base method can change this + # behavior by overriding the `should_generate_new_friendly_id?` method that + # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave + # more like 4.0. + # Note: Use(include) Slugged module in the config if using the anonymous module. + # If you have `friendly_id :name, use: slugged` in the model, Slugged module + # is included after the anonymous module defined in the initializer, so it + # overrides the `should_generate_new_friendly_id?` method from the anonymous module. + # + # config.use :slugged + # config.use Module.new { + # def should_generate_new_friendly_id? + # slug.blank? || _changed? + # end + # } + # + # FriendlyId uses Rails's `parameterize` method to generate slugs, but for + # languages that don't use the Roman alphabet, that's not usually sufficient. + # Here we use the Babosa library to transliterate Russian Cyrillic slugs to + # ASCII. If you use this, don't forget to add "babosa" to your Gemfile. + # + # config.use Module.new { + # def normalize_friendly_id(text) + # text.to_slug.normalize! :transliterations => [:russian, :latin] + # end + # } +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4be7b91979..a23fb1ac6d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1129,6 +1129,7 @@ en: logout: Log Out log_in: Log In log_in_tooltip: Log in with an existing account + microcosms: Microcosms sign_up: Sign Up start_mapping: Start Mapping sign_up_tooltip: Create an account for editing @@ -1168,6 +1169,14 @@ en: text: Make a Donation learn_more: "Learn More" more: More + microcosms: + index: + new: "New" + new_title: "Create a new microcosm" + microcosm: + join: + action: "Join" + confirm: "Join %{name}?" notifier: diary_comment_notification: subject: "[OpenStreetMap] %{user} commented on a diary entry" diff --git a/config/routes.rb b/config/routes.rb index 002ee58ea3..a707c53df4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -318,6 +318,14 @@ # redactions resources :redactions + # microcosms + resources :microcosms + resources :microcosm_member, :only => [:create, :edit, :new, :update] + resources :events + resources :event_attendances + get "microcosms/:id/members", :to => "microcosms#show_members", :as => :members_of_microcosm + get "microcosms/:id/events", :to => "microcosms#show_events", :as => :events_of_microcosm + # errors match "/403", :to => "errors#forbidden", :via => :all match "/404", :to => "errors#not_found", :via => :all diff --git a/db/migrate/20190518115041_add_acl_indexes.rb b/db/migrate/20190518115041_add_acl_indexes.rb index a66d820bcc..12f98c0ec3 100644 --- a/db/migrate/20190518115041_add_acl_indexes.rb +++ b/db/migrate/20190518115041_add_acl_indexes.rb @@ -1,6 +1,6 @@ class AddAclIndexes < ActiveRecord::Migration[5.2] def change add_index :acls, :domain - add_index :acls, :address, :using => :gist, :opclass => :inet_ops + add_index :acls, :address, :using => :gist, :opclass => :gist_inet_ops end end diff --git a/db/migrate/20190826032448_create_microcosms.rb b/db/migrate/20190826032448_create_microcosms.rb new file mode 100644 index 0000000000..2247fdf6cc --- /dev/null +++ b/db/migrate/20190826032448_create_microcosms.rb @@ -0,0 +1,13 @@ +class CreateMicrocosms < ActiveRecord::Migration[5.2] + def change + create_table :microcosms do |t| + t.string :name, :null => false + t.string :key, :null => false + t.string :facebook + t.string :twitter + t.text :description + + t.timestamps + end + end +end diff --git a/db/migrate/20190831122812_create_microcosm_members.rb b/db/migrate/20190831122812_create_microcosm_members.rb new file mode 100644 index 0000000000..8736dd07c6 --- /dev/null +++ b/db/migrate/20190831122812_create_microcosm_members.rb @@ -0,0 +1,22 @@ +class CreateMicrocosmMembers < ActiveRecord::Migration[5.2] + def change + create_table :microcosm_members do |t| + t.integer :microcosm_id, :null => false, :index => true + t.integer :user_id, :null => false, :index => true + t.string :role, :limit => 64, :null => false + t.timestamps + end + end +end + +class AddMicrocosmMemberFkToUser < ActiveRecord::Migration[5.2] + def change + add_foreign_key :microcosm_members, :user, validate: false + end +end + +class ValidateMicrocosmMemberFkToUser < ActiveRecord::Migration[5.2] + def change + validate_foreign_key :microcosm_members, :user + end +end diff --git a/db/migrate/20190901143302_create_friendly_id_slugs.rb b/db/migrate/20190901143302_create_friendly_id_slugs.rb new file mode 100644 index 0000000000..362a617402 --- /dev/null +++ b/db/migrate/20190901143302_create_friendly_id_slugs.rb @@ -0,0 +1,14 @@ +class CreateFriendlyIdSlugs < ActiveRecord::Migration[5.2] + def change + create_table :friendly_id_slugs do |t| + t.string :slug, :null => false + t.integer :sluggable_id, :null => false + t.string :sluggable_type, :limit => 50 + t.string :scope + t.datetime :created_at + end + add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id] + add_index :friendly_id_slugs, [:slug, :sluggable_type], :length => { :slug => 140, :sluggable_type => 50 } + add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], :length => { :slug => 70, :sluggable_type => 50, :scope => 70 }, :unique => true + end +end diff --git a/db/migrate/20190901151436_add_slug_to_microcosms.rb b/db/migrate/20190901151436_add_slug_to_microcosms.rb new file mode 100644 index 0000000000..59b1077edc --- /dev/null +++ b/db/migrate/20190901151436_add_slug_to_microcosms.rb @@ -0,0 +1,20 @@ +class AddSlugToMicrocosms < ActiveRecord::Migration[5.2] + def up + add_column :microcosms, :slug, :string + Microcosm.update_all ["slug = key"] + change_column_null :microcosms, :slug, false + end + + def down + Microcosm.update_all ["key = slug"] + remove_column :microcosms, :slug + end +end + +class AddIndexToMicrocosmSlug < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :microcosms, :slug, :unique => true, :algorithm => :concurrently + end +end diff --git a/db/migrate/20190901163613_remove_key_from_microsoms.rb b/db/migrate/20190901163613_remove_key_from_microsoms.rb new file mode 100644 index 0000000000..ef3f0e2804 --- /dev/null +++ b/db/migrate/20190901163613_remove_key_from_microsoms.rb @@ -0,0 +1,5 @@ +class RemoveKeyFromMicrosoms < ActiveRecord::Migration[5.2] + def change + safety_assured { remove_column :microcosms, :key } # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20190902200639_create_microcosm_links.rb b/db/migrate/20190902200639_create_microcosm_links.rb new file mode 100644 index 0000000000..120c2b3637 --- /dev/null +++ b/db/migrate/20190902200639_create_microcosm_links.rb @@ -0,0 +1,23 @@ +class CreateMicrocosmLinks < ActiveRecord::Migration[5.2] + def change + create_table :microcosm_links do |t| + t.integer :microcosm_id, :null => false, :index => true + t.string :site, :null => false + t.string :url, :null => false + + t.timestamps + end + end +end + +class CreateMicrocosmLinksFk < ActiveRecord::Migration[5.2] + def change + add_foreign_key :microcosm_links, :microcosm, validate: false + end +end + +class ValidateMicrocosmLinksFk < ActiveRecord::Migration[5.2] + def change + validate_foreign_key :microcosm_links, :microcosm + end +end diff --git a/db/migrate/20190902234710_remove_facebook_and_twitter_from_microsoms.rb b/db/migrate/20190902234710_remove_facebook_and_twitter_from_microsoms.rb new file mode 100644 index 0000000000..56e9e3fa54 --- /dev/null +++ b/db/migrate/20190902234710_remove_facebook_and_twitter_from_microsoms.rb @@ -0,0 +1,6 @@ +class RemoveFacebookAndTwitterFromMicrosoms < ActiveRecord::Migration[5.2] + def change + safety_assured { remove_column :microcosms, :facebook } # rubocop:disable Rails/ReversibleMigration + safety_assured { remove_column :microcosms, :twitter } # rubocop:disable Rails/ReversibleMigration + end +end diff --git a/db/migrate/20190903023243_role_in_microcosm_should_be_unique.rb b/db/migrate/20190903023243_role_in_microcosm_should_be_unique.rb new file mode 100644 index 0000000000..4d002e446a --- /dev/null +++ b/db/migrate/20190903023243_role_in_microcosm_should_be_unique.rb @@ -0,0 +1,7 @@ +class RoleInMicrocosmShouldBeUnique < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :microcosm_members, [:microcosm_id, :user_id, :role], :unique => true, :algorithm => :concurrently + end +end diff --git a/db/migrate/20190903030453_add_location_to_microcosms.rb b/db/migrate/20190903030453_add_location_to_microcosms.rb new file mode 100644 index 0000000000..0fadb8c423 --- /dev/null +++ b/db/migrate/20190903030453_add_location_to_microcosms.rb @@ -0,0 +1,16 @@ +class AddLocationToMicrocosms < ActiveRecord::Migration[5.2] + def change + # This group of migrations for microcosms will be run together. + safety_assured do + change_table "microcosms", :bulk => true do |t| + t.string "location", :null => false + t.decimal "lat", :null => false + t.decimal "lon", :null => false + t.integer "min_lat", :null => false + t.integer "max_lat", :null => false + t.integer "min_lon", :null => false + t.integer "max_lon", :null => false + end + end + end +end diff --git a/db/migrate/20190905160802_create_events.rb b/db/migrate/20190905160802_create_events.rb new file mode 100644 index 0000000000..42171ba803 --- /dev/null +++ b/db/migrate/20190905160802_create_events.rb @@ -0,0 +1,25 @@ +class CreateEvents < ActiveRecord::Migration[5.2] + def change + create_table :events do |t| + t.string :title, :null => false + t.datetime :moment + t.string :location + t.text :description + t.integer :microcosm_id + + t.timestamps + end + end +end + +class CreateEventsFk < ActiveRecord::Migration[5.2] + def change + add_foreign_key :events, :microcosm, validate: false + end +end + +class ValidateEventsFk < ActiveRecord::Migration[5.2] + def change + validate_foreign_key :events, :microcosm + end +end diff --git a/db/migrate/20190905224243_create_event_attendances.rb b/db/migrate/20190905224243_create_event_attendances.rb new file mode 100644 index 0000000000..65980c0ee3 --- /dev/null +++ b/db/migrate/20190905224243_create_event_attendances.rb @@ -0,0 +1,25 @@ +class CreateEventAttendances < ActiveRecord::Migration[5.2] + def change + create_table :event_attendances do |t| + t.integer :user_id, :null => false, :index => true + t.integer :event_id, :null => false, :index => true + t.string :intention, :null => false + + t.timestamps + end + end +end + +class CreateEventAttendancesFk < ActiveRecord::Migration[5.2] + def change + add_foreign_key :event_attendances, :user, validate: false + add_foreign_key :event_attendances, :event, validate: false + end +end + +class ValidateEventAttendancesFk < ActiveRecord::Migration[5.2] + def change + validate_foreign_key :event_attendances, :user + validate_foreign_key :event_attendances, :event + end +end diff --git a/db/structure.sql b/db/structure.sql index 47f3cf7a06..a94a2690f0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8,20 +8,6 @@ SET check_function_bodies = false; SET client_min_messages = warning; SET row_security = off; --- --- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; - - --- --- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; - - -- -- Name: btree_gist; Type: EXTENSION; Schema: -; Owner: - -- @@ -128,33 +114,6 @@ CREATE TYPE public.user_status_enum AS ENUM ( ); --- --- Name: maptile_for_point(bigint, bigint, integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.maptile_for_point(bigint, bigint, integer) RETURNS integer - LANGUAGE c STRICT - AS '$libdir/libpgosm.so', 'maptile_for_point'; - - --- --- Name: tile_for_point(integer, integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.tile_for_point(integer, integer) RETURNS bigint - LANGUAGE c STRICT - AS '$libdir/libpgosm.so', 'tile_for_point'; - - --- --- Name: xid_to_int4(xid); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.xid_to_int4(xid) RETURNS integer - LANGUAGE c IMMUTABLE STRICT - AS '$libdir/libpgosm.so', 'xid_to_int4'; - - SET default_tablespace = ''; SET default_with_oids = false; @@ -684,6 +643,107 @@ CREATE TABLE public.diary_entry_subscriptions ( ); +-- +-- Name: event_attendances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.event_attendances ( + id bigint NOT NULL, + user_id integer NOT NULL, + event_id integer NOT NULL, + intention character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: event_attendances_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.event_attendances_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: event_attendances_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.event_attendances_id_seq OWNED BY public.event_attendances.id; + + +-- +-- Name: events; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.events ( + id bigint NOT NULL, + title character varying NOT NULL, + moment timestamp without time zone, + location character varying, + description text, + microcosm_id integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: events_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.events_id_seq OWNED BY public.events.id; + + +-- +-- Name: friendly_id_slugs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.friendly_id_slugs ( + id bigint NOT NULL, + slug character varying NOT NULL, + sluggable_id integer NOT NULL, + sluggable_type character varying(50), + scope character varying, + created_at timestamp without time zone +); + + +-- +-- Name: friendly_id_slugs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.friendly_id_slugs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: friendly_id_slugs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.friendly_id_slugs_id_seq OWNED BY public.friendly_id_slugs.id; + + -- -- Name: friends; Type: TABLE; Schema: public; Owner: - -- @@ -917,6 +977,112 @@ CREATE SEQUENCE public.messages_id_seq ALTER SEQUENCE public.messages_id_seq OWNED BY public.messages.id; +-- +-- Name: microcosm_links; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.microcosm_links ( + id bigint NOT NULL, + microcosm_id integer NOT NULL, + site character varying NOT NULL, + url character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: microcosm_links_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.microcosm_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: microcosm_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.microcosm_links_id_seq OWNED BY public.microcosm_links.id; + + +-- +-- Name: microcosm_members; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.microcosm_members ( + id bigint NOT NULL, + microcosm_id integer NOT NULL, + user_id integer NOT NULL, + role character varying(64) NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: microcosm_members_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.microcosm_members_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: microcosm_members_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.microcosm_members_id_seq OWNED BY public.microcosm_members.id; + + +-- +-- Name: microcosms; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.microcosms ( + id bigint NOT NULL, + name character varying NOT NULL, + description text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + slug character varying NOT NULL, + location character varying NOT NULL, + lat numeric NOT NULL, + lon numeric NOT NULL, + min_lat integer NOT NULL, + max_lat integer NOT NULL, + min_lon integer NOT NULL, + max_lon integer NOT NULL +); + + +-- +-- Name: microcosms_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.microcosms_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: microcosms_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.microcosms_id_seq OWNED BY public.microcosms.id; + + -- -- Name: node_tags; Type: TABLE; Schema: public; Owner: - -- @@ -1505,6 +1671,27 @@ ALTER TABLE ONLY public.diary_comments ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.diary_entries ALTER COLUMN id SET DEFAULT nextval('public.diary_entries_id_seq'::regclass); +-- +-- Name: event_attendances id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances ALTER COLUMN id SET DEFAULT nextval('public.event_attendances_id_seq'::regclass); + + +-- +-- Name: events id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.events ALTER COLUMN id SET DEFAULT nextval('public.events_id_seq'::regclass); + + +-- +-- Name: friendly_id_slugs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.friendly_id_slugs ALTER COLUMN id SET DEFAULT nextval('public.friendly_id_slugs_id_seq'::regclass); + + -- -- Name: friends id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1547,6 +1734,27 @@ ALTER TABLE ONLY public.issues ALTER COLUMN id SET DEFAULT nextval('public.issue ALTER TABLE ONLY public.messages ALTER COLUMN id SET DEFAULT nextval('public.messages_id_seq'::regclass); +-- +-- Name: microcosm_links id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosm_links ALTER COLUMN id SET DEFAULT nextval('public.microcosm_links_id_seq'::regclass); + + +-- +-- Name: microcosm_members id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosm_members ALTER COLUMN id SET DEFAULT nextval('public.microcosm_members_id_seq'::regclass); + + +-- +-- Name: microcosms id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosms ALTER COLUMN id SET DEFAULT nextval('public.microcosms_id_seq'::regclass); + + -- -- Name: note_comments id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1769,6 +1977,30 @@ ALTER TABLE ONLY public.diary_entry_subscriptions ADD CONSTRAINT diary_entry_subscriptions_pkey PRIMARY KEY (user_id, diary_entry_id); +-- +-- Name: event_attendances event_attendances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.event_attendances + ADD CONSTRAINT event_attendances_pkey PRIMARY KEY (id); + + +-- +-- Name: events events_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.events + ADD CONSTRAINT events_pkey PRIMARY KEY (id); + + +-- +-- Name: friendly_id_slugs friendly_id_slugs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.friendly_id_slugs + ADD CONSTRAINT friendly_id_slugs_pkey PRIMARY KEY (id); + + -- -- Name: friends friends_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1825,6 +2057,30 @@ ALTER TABLE ONLY public.messages ADD CONSTRAINT messages_pkey PRIMARY KEY (id); +-- +-- Name: microcosm_links microcosm_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosm_links + ADD CONSTRAINT microcosm_links_pkey PRIMARY KEY (id); + + +-- +-- Name: microcosm_members microcosm_members_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosm_members + ADD CONSTRAINT microcosm_members_pkey PRIMARY KEY (id); + + +-- +-- Name: microcosms microcosms_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.microcosms + ADD CONSTRAINT microcosms_pkey PRIMARY KEY (id); + + -- -- Name: node_tags node_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2164,7 +2420,7 @@ CREATE INDEX gpx_files_visible_visibility_idx ON public.gpx_files USING btree (v -- Name: index_acls_on_address; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX index_acls_on_address ON public.acls USING gist (address inet_ops); +CREATE INDEX index_acls_on_address ON public.acls USING gist (address); -- @@ -2244,6 +2500,41 @@ CREATE INDEX index_client_applications_on_user_id ON public.client_applications CREATE INDEX index_diary_entry_subscriptions_on_diary_entry_id ON public.diary_entry_subscriptions USING btree (diary_entry_id); +-- +-- Name: index_event_attendances_on_event_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_event_attendances_on_event_id ON public.event_attendances USING btree (event_id); + + +-- +-- Name: index_event_attendances_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_event_attendances_on_user_id ON public.event_attendances USING btree (user_id); + + +-- +-- Name: index_friendly_id_slugs_on_slug_and_sluggable_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_friendly_id_slugs_on_slug_and_sluggable_type ON public.friendly_id_slugs USING btree (slug, sluggable_type); + + +-- +-- Name: index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope ON public.friendly_id_slugs USING btree (slug, sluggable_type, scope); + + +-- +-- Name: index_friendly_id_slugs_on_sluggable_type_and_sluggable_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_friendly_id_slugs_on_sluggable_type_and_sluggable_id ON public.friendly_id_slugs USING btree (sluggable_type, sluggable_id); + + -- -- Name: index_issue_comments_on_issue_id; Type: INDEX; Schema: public; Owner: - -- @@ -2293,6 +2584,34 @@ CREATE INDEX index_issues_on_status ON public.issues USING btree (status); CREATE INDEX index_issues_on_updated_by ON public.issues USING btree (updated_by); +-- +-- Name: index_microcosm_links_on_microcosm_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_microcosm_links_on_microcosm_id ON public.microcosm_links USING btree (microcosm_id); + + +-- +-- Name: index_microcosm_members_on_microcosm_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_microcosm_members_on_microcosm_id ON public.microcosm_members USING btree (microcosm_id); + + +-- +-- Name: index_microcosm_members_on_microcosm_id_and_user_id_and_role; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_microcosm_members_on_microcosm_id_and_user_id_and_role ON public.microcosm_members USING btree (microcosm_id, user_id, role); + + +-- +-- Name: index_microcosm_members_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_microcosm_members_on_user_id ON public.microcosm_members USING btree (user_id); + + -- -- Name: index_note_comments_on_body; Type: INDEX; Schema: public; Owner: - -- @@ -3084,6 +3403,17 @@ INSERT INTO "schema_migrations" (version) VALUES ('20190623093642'), ('20190702193519'), ('20190716173946'), +('20190826032448'), +('20190831122812'), +('20190901143302'), +('20190901151436'), +('20190901163613'), +('20190902200639'), +('20190902234710'), +('20190903023243'), +('20190903030453'), +('20190905160802'), +('20190905224243'), ('21'), ('22'), ('23'), diff --git a/features/admin.feature b/features/admin.feature new file mode 100644 index 0000000000..f4fde528a5 --- /dev/null +++ b/features/admin.feature @@ -0,0 +1,36 @@ +Feature: Administer a Microcosm + In order to administer microcosms + as an administrator + I want to create the microcosm + + Background: + Given there is a microcosm "MappingDC", "Washington, DC, USA", "38.9", "-77.03", "38.516 * 10**7", "39.472 * 10**7", "-77.671 * 10**7", "-76.349 * 10**7" + And the microcosm has description "MappingDC strives to improve OSM in the DC area" + And the microcosm has the "Facebook" page "https://facebook.com/groups/mappingdc" + And the microcosm has the "Twitter" page "https://twitter.com/mappingdc" + And the microcosm has the "Website" page "https://mappingdc.org" + And I am on the microcosm "MappingDC" page + + + Scenario: Create a microcosm + Given there is a user "abe@example.com" with name "Abe" + And "abe@example.com" is an administrator + When user "abe@example.com" logs in + And I am on the microcosms page + And I click the link to "/microcosms/new" + And I set the microcosm to "#new_microcosm", "Baltimore", "38", "-77" + And I submit the form + Then I should see "Baltimore" + + Scenario: Promote a user to organizer + Given there is a user "orlando@example.com" with name "Orlando" + And the user belongs to the microcosm + Given there is a user "abe@example.com" with name "Abe" + And "abe@example.com" is an administrator + When user "abe@example.com" logs in + And I am on the microcosm "MappingDC" page + And I click "Members" + And I click "edit" + And I set the user to "Organizer" + And I submit the form + Then I should see "Organizers Orlando" \ No newline at end of file diff --git a/features/member.feature b/features/member.feature new file mode 100644 index 0000000000..2f3d970281 --- /dev/null +++ b/features/member.feature @@ -0,0 +1,27 @@ +Feature: Interact with the Microcosm + In order to interact with a microcosm + as a member + I want to perform member actions + + Background: + Given there is a microcosm "MappingDC", "Washington, DC, USA", "38.9", "-77.03", "38.516 * 10**7", "39.472 * 10**7", "-77.671 * 10**7", "-76.349 * 10**7" + And the microcosm has description "MappingDC strives to improve OSM in the DC area" + And the microcosm has the "Facebook" page "https://facebook.com/groups/mappingdc" + And the microcosm has the "Twitter" page "https://twitter.com/mappingdc" + And the microcosm has the "Website" page "https://mappingdc.org" + And I am on the microcosm "MappingDC" page + + Scenario: RSVP for an event + Given there is an event for this microcosm + And there is a user "will_attend@example.com" with name "Will" + And this user is an organizer of this microcosm + And user "will_attend@example.com" logs in + And I am on this event page + Then I should see "0 people are going." + Then I should see "Are you going?" + And I press "Yes" + And I am on this event page + Then I should see "1 people are going." + And I press "No" + And I am on this event page + Then I should see "0 people are going." diff --git a/features/organizer.feature b/features/organizer.feature new file mode 100644 index 0000000000..1a249badaf --- /dev/null +++ b/features/organizer.feature @@ -0,0 +1,49 @@ +Feature: Manage a Microcosm + In order to manage microcosms + as an organizer + I want to manage the microcosm + + Background: + Given there is a microcosm "MappingDC", "Washington, DC, USA", "38.9", "-77.03", "38.516 * 10**7", "39.472 * 10**7", "-77.671 * 10**7", "-76.349 * 10**7" + And the microcosm has description "MappingDC strives to improve OSM in the DC area" + And the microcosm has the "Facebook" page "https://facebook.com/groups/mappingdc" + And the microcosm has the "Twitter" page "https://twitter.com/mappingdc" + And the microcosm has the "Website" page "https://mappingdc.org" + And I am on the microcosm "MappingDC" page + + + Scenario: Edit a microcosm + Given there is a user "abe@example.com" with name "Abe" + And this user is an organizer of this microcosm + When user "abe@example.com" logs in + And I am on the microcosms page + And I click the link to "/microcosms/mappingdc/edit" + And I set the microcosm to ".edit_microcosm", "Baltimore", "40", "-76" + And I submit the form + Then I should not see "Washington, DC, USA" + Then I should see "Baltimore" + + Scenario: Promote a user to organizer + Given there is a user "organizer@example.com" with name "Organizer" + And this user is an organizer of this microcosm + Given there is a user "promotee@example.com" with name "Promotee" + And the user belongs to the microcosm + When user "organizer@example.com" logs in + And I am on the microcosm "MappingDC" page + And I click "Members" + And Within ".members" I click the 2 "edit" + And I set the user to "Organizer" + And I submit the form + Then I should see "Organizers Organizer Promotee" + + Scenario: Create an event + Given there is a user "abe@example.com" with name "Abe" + And this user is an organizer of this microcosm + When user "abe@example.com" logs in + And I am on the microcosm "MappingDC" page + And I click "Upcoming Events" + And I click "new event" + And I set the event to "Update DC Bike Lanes", "DC Library", "We will update the dc bike lane data in OSM." + And I submit the form + And I am on the microcosm "MappingDC" page + Then I should see "Update DC Bike Lanes" diff --git a/features/step_definitions/.gitkeep b/features/step_definitions/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/step_definitions/about_spec.rb b/features/step_definitions/about_spec.rb new file mode 100644 index 0000000000..52d2bc51ce --- /dev/null +++ b/features/step_definitions/about_spec.rb @@ -0,0 +1,165 @@ +Given("there is a microcosm {string}, {string}, {string}, {string}, {string}, {string}, {string}, {string}") do |name, location, lat, lon, min_lat, max_lat, min_lon, max_lon| + @the_microcosm = Microcosm.create!( + :name => name, + :location => location, + :lat => lat, + :lon => lon, + :min_lat => min_lat, + :min_lon => min_lon, + :max_lat => max_lat, + :max_lon => max_lon + ) +end + +Given("I am on the microcosms page") do + visit "/microcosms" +end + +Given("I am on the microcosm {string} page") do |name| + visit "/microcosms/" + name.downcase +end + +Given("I am on the microcosm page by id") do + visit "/microcosms/#{@the_microcosm.id}" +end + +Given("I am on the microcosm edit page") do + visit "/microcosms/#{@the_microcosm.id}/edit" +end + +Given("there is an event for this microcosm") do + @the_event = Event.create!( + :title => "Some Event", + :moment => DateTime.now, + :location => "Some Location", + :description => "Some description", + :microcosm_id => @the_microcosm.id + ) +end + +Given("I am on this event page") do + visit event_path(@the_event) +end + +# The lines like "The microcosm HAS..." are not behavior driven because it's using @varibles. + +Given("the microcosm has the {string} page {string}") do |site, url| + @the_microcosm.set_link(site, url) + @the_microcosm.save +end + +Given("the microcosm has description {string}") do |desc| + @the_microcosm.description = desc + @the_microcosm.save +end + +Given("the user belongs to the microcosm") do + @the_microcosm.microcosm_members.create!(:user_id => @the_user.id, :role => MicrocosmMember::Roles::MEMBER) +end + +Given("this user is an organizer of this microcosm") do + @the_microcosm.microcosm_members.create!(:user_id => @the_user.id, :role => MicrocosmMember::Roles::ORGANIZER) +end + +Then("I should see the microcosm {string} name") do |name| + expect(page).to have_content(name) +end + +And("I set the microcosm to {string}, {string}, {string}, {string}") do |scope, name, lat, lon| + within(scope) do + fill_in "Name", :with => name + fill_in "Location", :with => name + fill_in "Lat", :with => lat + fill_in "Lon", :with => lon + fill_in "Min lat", :with => lat + fill_in "Max lat", :with => lat + fill_in "Min lon", :with => lon + fill_in "Max lon", :with => lon + fill_in "Description", :with => name + end +end + +And("I set the event to {string}, {string}, {string}") do |title, location, description| + within("#content") do + fill_in "Title", :with => title + fill_in "Location", :with => location + fill_in "Description", :with => description + end +end + +And("I set the user to {string}") do |role| + within("#content") do + select role, :from => "Role" + end +end + +And("I submit the form") do + within("#content") do + find('form input[type="submit"]').click + end +end + +# Not microcosm specific. + +Given("{string} is an administrator") do |email| + user = User.find_by(:email => email) + user.roles.create(:role => "administrator", :granter => user) + user.save +end + +When("print body") do + print body +end + +Then("I should see the {string} link to {string}") do |title, href| + expect(page).to have_link(title, :href => href) +end + +Then("I should see {string}") do |msg| + expect(page).to have_content(msg) +end + +Then("I should not see {string}") do |msg| + expect(page).not_to have_content(msg) +end + +Then("I should see a {string} button") do |title| + expect(page).to have_selector(:link_or_button, title) +end + +Then("I should be forbidden") do + expect(page.status_code).to eq(403) +end + +And("I click {string}") do |title| + within("#content") do + click_link(title) + end +end + +And("Within {string} I click the {int} {string}") do |scope, nth, text| + within(scope) do + find(:xpath, "(.//a[contains(text(), #{text})])[#{nth}]").click + end +end + +And("I click the link to {string}") do |url| + find("a[href='#{url}']").click +end + +And("I press {string}") do |title| + click_button title +end + +When("user {string} logs in") do |username| + visit "/login" + within("#login_form") do + fill_in "username", :with => username + fill_in "password", :with => "test" + click_button "Login" + end +end + +Given("there is a user {string} with name {string}") do |username, name| + @the_user = create(:user, :email => username, :display_name => name) +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000000..ddcf1c3888 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,61 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +require "cucumber/rails" + +# frozen_string_literal: true + +# Capybara defaults to CSS3 selectors rather than XPath. +# If you'd prefer to use XPath, just uncomment this line and adjust any +# selectors in your step definitions to use the XPath syntax. +# Capybara.default_selector = :xpath + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + DatabaseCleaner.strategy = :transaction +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { except: [:widgets] } may not do what you expect here +# # as Cucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('not @no-txn', 'not @selenium', 'not @culerity', 'not @celerity', 'not @javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation + +World(FactoryBot::Syntax::Methods) diff --git a/features/visitor_about.feature b/features/visitor_about.feature new file mode 100644 index 0000000000..069ca33f99 --- /dev/null +++ b/features/visitor_about.feature @@ -0,0 +1,53 @@ +Feature: Learn about the Microcosm + In order to learn about this microcosm + as a visitor + I want to read their webpage + + Background: + Given there is a microcosm "MappingDC", "Washington, DC, USA", "38.9", "-77.03", "38.516 * 10**7", "39.472 * 10**7", "-77.671 * 10**7", "-76.349 * 10**7" + And the microcosm has description "MappingDC strives to improve OSM in the DC area" + And the microcosm has the "Facebook" page "https://facebook.com/groups/mappingdc" + And the microcosm has the "Twitter" page "https://twitter.com/mappingdc" + And the microcosm has the "Website" page "https://mappingdc.org" + And I am on the microcosm "MappingDC" page + + + Scenario: The microcosm should be listed + When I am on the microcosms page + Then I should see "MappingDC" + + + Scenario: Describe the microcosm + Then I should see the microcosm "MappingDC" name + Then I should see "Washington, DC, USA" + Then I should see the "Facebook" link to "https://facebook.com/groups/mappingdc" + Then I should see the "Twitter" link to "https://twitter.com/mappingdc" + Then I should see the "Website" link to "https://mappingdc.org" + Then I should see "MappingDC strives to improve OSM in the DC area" + + + Scenario: Can load by id + Then I am on the microcosm page by id + Then I should see "MappingDC strives to improve OSM in the DC area" + + + Scenario: Regular user cannot edit the microcosm + Given there is a user "abe@example.com" with name "Abe" + When user "abe@example.com" logs in + When I am on the microcosm edit page + Then I should be forbidden + + + Scenario: Logged out user sees message to join microcosm + Given there is a user "abe@example.com" with name "Abe" + When I am on the microcosm "MappingDC" page + Then I press "Join" + + + Scenario: A user may join a microcosm + Given there is a user "abe@example.com" with name "Abraham" + When user "abe@example.com" logs in + And I am on the microcosm "MappingDC" page + And I should see a "Join" button + And I press "Join" + Then I should see "Abraham" \ No newline at end of file diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake new file mode 100644 index 0000000000..c9b13adc86 --- /dev/null +++ b/lib/tasks/cucumber.rake @@ -0,0 +1,75 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +unless ARGV.any? { |a| a =~ /^gems/ } # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir[Rails.root.join("vendor", "{gems,plugins}", "cucumber*", "bin", "cucumber")].first # rubocop:disable Layout/IndentationWidth +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + "/../lib") unless vendored_cucumber_bin.nil? + +begin + require "cucumber/rake/task" + + namespace :cucumber do + Cucumber::Rake::Task.new({ :ok => "test:prepare" }, "Run features that should pass") do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = "default" + end + + Cucumber::Rake::Task.new({ :wip => "test:prepare" }, "Run features that are being worked on") do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = "wip" + end + + Cucumber::Rake::Task.new({ :rerun => "test:prepare" }, "Record failing features and run only them if any exist") do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = "rerun" + end + + desc "Run all features" + task :all => [:ok, :wip] + + task :statsetup do + require "rails/code_statistics" + ::STATS_DIRECTORIES << %w[Cucumber\ features features] if File.exist?("features") + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?("features") + end + + task :annotations_setup do + Rails.application.configure do + if config.respond_to?(:annotations) + config.annotations.directories << "features" + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + end + end + end + end + desc "Alias for cucumber:ok" + task :cucumber => "cucumber:ok" + + task :default => :cucumber + + task :features => :cucumber do + warn "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. + task "test:prepare" do + end + + task :stats => "cucumber:statsetup" + + task :notes => "cucumber:annotations_setup" +rescue LoadError + desc "cucumber rake task not available (cucumber not installed)" + task :cucumber do + abort "Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin" + end +end + +end diff --git a/script/cucumber b/script/cucumber new file mode 100755 index 0000000000..3d7910bdf6 --- /dev/null +++ b/script/cucumber @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require "rubygems" unless ENV["NO_RUBYGEMS"] + require "cucumber" + load Cucumber::BINARY +end diff --git a/test/controllers/event_attendances_controller_test.rb b/test/controllers/event_attendances_controller_test.rb new file mode 100644 index 0000000000..4496227057 --- /dev/null +++ b/test/controllers/event_attendances_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventAttendancesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb new file mode 100644 index 0000000000..d2243abe1a --- /dev/null +++ b/test/controllers/events_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/microcosm_member_controller_test.rb b/test/controllers/microcosm_member_controller_test.rb new file mode 100644 index 0000000000..c9c1466b75 --- /dev/null +++ b/test/controllers/microcosm_member_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MicrocosmMemberControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/microcosms_controller_test.rb b/test/controllers/microcosms_controller_test.rb new file mode 100644 index 0000000000..5c9bf0079d --- /dev/null +++ b/test/controllers/microcosms_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MicrocosmsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/factories/event_attendances.rb b/test/factories/event_attendances.rb new file mode 100644 index 0000000000..f096943de0 --- /dev/null +++ b/test/factories/event_attendances.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :event_attendance do + user_id { 1 } + event_id { 1 } + intention { "MyString" } + end +end diff --git a/test/factories/events.rb b/test/factories/events.rb new file mode 100644 index 0000000000..de27fcaf17 --- /dev/null +++ b/test/factories/events.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :event do + title { "MyString" } + moment { "2019-09-05 12:08:02" } + location { "MyString" } + description { "MyText" } + microcosm_id { 1 } + end +end diff --git a/test/factories/microcosm_links.rb b/test/factories/microcosm_links.rb new file mode 100644 index 0000000000..e0ecda3366 --- /dev/null +++ b/test/factories/microcosm_links.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :microcosm_link do + microcosm_id { 1 } + site { "MyString" } + url { "MyString" } + end +end diff --git a/test/factories/microcosm_members.rb b/test/factories/microcosm_members.rb new file mode 100644 index 0000000000..3e3d364450 --- /dev/null +++ b/test/factories/microcosm_members.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :microcosm_member do + microcosm_id { 1 } + user_id { 1 } + role { "MyString" } + end +end diff --git a/test/factories/microcosms.rb b/test/factories/microcosms.rb new file mode 100644 index 0000000000..3a93b0bd7a --- /dev/null +++ b/test/factories/microcosms.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :microcosm do + name { "MyString" } + key { "MyString" } + facebook { "MyString" } + twitter { "MyString" } + description { "MyText" } + end +end diff --git a/test/models/event_attendance_test.rb b/test/models/event_attendance_test.rb new file mode 100644 index 0000000000..dd8c492497 --- /dev/null +++ b/test/models/event_attendance_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventAttendanceTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 0000000000..c8465c19ee --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/microcosm_link_test.rb b/test/models/microcosm_link_test.rb new file mode 100644 index 0000000000..297ff564f9 --- /dev/null +++ b/test/models/microcosm_link_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MicrocosmLinkTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/microcosm_member_test.rb b/test/models/microcosm_member_test.rb new file mode 100644 index 0000000000..58a24b0a4f --- /dev/null +++ b/test/models/microcosm_member_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MicrocosmMemberTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/microcosm_test.rb b/test/models/microcosm_test.rb new file mode 100644 index 0000000000..93ec81fc06 --- /dev/null +++ b/test/models/microcosm_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class MicrocosmTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end