├── web ├── templates │ ├── session │ │ ├── email_sent.html.eex │ │ ├── register_new_user.html.eex │ │ └── index.html.eex │ ├── email │ │ └── notification.html.eex │ ├── account │ │ └── _address_show.html.eex │ ├── workflow │ │ ├── workflow │ │ │ └── new.html.eex │ │ └── section │ │ │ └── new.html.eex │ ├── attachment │ │ ├── edit.html.eex │ │ ├── new.html.eex │ │ ├── index.html.eex │ │ └── as_card_list.html.eex │ ├── tag │ │ ├── index.html.eex │ │ ├── edit.html.eex │ │ ├── new.html.eex │ │ └── _show_card.html.eex │ ├── user │ │ └── index.html.eex │ ├── reminder │ │ └── new.html.eex │ └── activity │ │ └── index.html.eex ├── views │ ├── email_view.ex │ ├── page_view.ex │ ├── layout_view.ex │ ├── session_view.ex │ ├── tag_view.ex │ ├── reminder_view.ex │ ├── workflow │ │ ├── state_view.ex │ │ ├── field_view.ex │ │ ├── section_view.ex │ │ ├── workflow_view.ex │ │ └── instance_view.ex │ ├── project_view.ex │ ├── contact_view.ex │ ├── event_view.ex │ ├── deal_view.ex │ ├── user_view.ex │ ├── error_view.ex │ ├── activity_view.ex │ ├── timesheet_entry_view.ex │ ├── changeset_view.ex │ ├── error_helpers.ex │ ├── account_view.ex │ └── paginator_helper.ex ├── controllers │ ├── page_controller.ex │ ├── utils.ex │ ├── activity_controller.ex │ ├── paginator.ex │ ├── session_plug.ex │ └── workflow │ │ └── state_controller.ex ├── models │ ├── deal_stage.ex │ ├── account_status.ex │ ├── role.ex │ ├── workflow │ │ ├── enum.ex │ │ ├── state.ex │ │ ├── section.ex │ │ ├── value.ex │ │ ├── instance.ex │ │ └── field.ex │ ├── deal_tag.ex │ ├── event_tag.ex │ ├── account_tag.ex │ ├── contact_tag.ex │ ├── project_tag.ex │ ├── timesheet_entry_tag.ex │ ├── timesheet_status.ex │ ├── workflow.ex │ ├── address.ex │ ├── contact.ex │ ├── attachment.ex │ ├── user.ex │ ├── rule.ex │ ├── project.ex │ ├── event.ex │ ├── reminder.ex │ ├── timesheet.ex │ └── timesheet_entry.ex ├── gettext.ex ├── channels │ └── user_socket.ex └── web.ex ├── lib ├── carbon │ ├── repo.ex │ ├── release_tasks.ex │ ├── search_index.ex │ ├── mailer.ex │ ├── endpoint.ex │ └── email_notification_sender.ex └── carbon.ex ├── test ├── test_helper.exs ├── doc_test.exs ├── views │ ├── page_view_test.exs │ ├── layout_view_test.exs │ ├── error_view_test.exs │ └── account_view_test.exs ├── models │ ├── workflow │ │ ├── instance_test.exs │ │ ├── value_test.exs │ │ ├── enum_test.exs │ │ ├── section_test.exs │ │ ├── state_test.exs │ │ └── field_test.exs │ ├── address_test.exs │ ├── user_test.exs │ ├── account_test.exs │ ├── role_test.exs │ ├── deal_tag_test.exs │ ├── event_tag_test.exs │ ├── workflow_test.exs │ ├── account_status_test.exs │ ├── account_tag_test.exs │ ├── contact_tag_test.exs │ ├── deal_stage_test.exs │ ├── reminder_test.exs │ ├── project_tag_test.exs │ ├── event_test.exs │ ├── contact_test.exs │ ├── deal_test.exs │ ├── attachment_test.exs │ └── timesheet_test.exs └── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── priv ├── static │ ├── images │ │ └── avatars │ │ │ ├── male │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── 8.png │ │ │ ├── 9.png │ │ │ ├── 10.png │ │ │ ├── 100.png │ │ │ ├── 101.png │ │ │ ├── 102.png │ │ │ ├── 103.png │ │ │ ├── 104.png │ │ │ ├── 105.png │ │ │ ├── 106.png │ │ │ ├── 107.png │ │ │ ├── 108.png │ │ │ ├── 109.png │ │ │ ├── 11.png │ │ │ ├── 110.png │ │ │ ├── 111.png │ │ │ ├── 112.png │ │ │ ├── 113.png │ │ │ ├── 114.png │ │ │ ├── 115.png │ │ │ ├── 116.png │ │ │ ├── 117.png │ │ │ ├── 118.png │ │ │ ├── 119.png │ │ │ ├── 12.png │ │ │ ├── 120.png │ │ │ ├── 121.png │ │ │ ├── 122.png │ │ │ ├── 123.png │ │ │ ├── 124.png │ │ │ ├── 125.png │ │ │ ├── 126.png │ │ │ ├── 127.png │ │ │ ├── 128.png │ │ │ ├── 129.png │ │ │ ├── 13.png │ │ │ ├── 14.png │ │ │ ├── 15.png │ │ │ ├── 16.png │ │ │ ├── 17.png │ │ │ ├── 18.png │ │ │ ├── 19.png │ │ │ ├── 20.png │ │ │ ├── 21.png │ │ │ ├── 22.png │ │ │ ├── 23.png │ │ │ ├── 24.png │ │ │ ├── 25.png │ │ │ ├── 26.png │ │ │ ├── 27.png │ │ │ ├── 28.png │ │ │ ├── 29.png │ │ │ ├── 30.png │ │ │ ├── 31.png │ │ │ ├── 32.png │ │ │ ├── 33.png │ │ │ ├── 34.png │ │ │ ├── 35.png │ │ │ ├── 36.png │ │ │ ├── 37.png │ │ │ ├── 38.png │ │ │ ├── 39.png │ │ │ ├── 40.png │ │ │ ├── 41.png │ │ │ ├── 42.png │ │ │ ├── 43.png │ │ │ ├── 44.png │ │ │ ├── 45.png │ │ │ ├── 46.png │ │ │ ├── 47.png │ │ │ ├── 48.png │ │ │ ├── 49.png │ │ │ ├── 50.png │ │ │ ├── 51.png │ │ │ ├── 52.png │ │ │ ├── 53.png │ │ │ ├── 54.png │ │ │ ├── 55.png │ │ │ ├── 56.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 59.png │ │ │ ├── 60.png │ │ │ ├── 61.png │ │ │ ├── 62.png │ │ │ ├── 63.png │ │ │ ├── 64.png │ │ │ ├── 65.png │ │ │ ├── 66.png │ │ │ ├── 67.png │ │ │ ├── 68.png │ │ │ ├── 69.png │ │ │ ├── 70.png │ │ │ ├── 71.png │ │ │ ├── 72.png │ │ │ ├── 73.png │ │ │ ├── 74.png │ │ │ ├── 75.png │ │ │ ├── 76.png │ │ │ ├── 77.png │ │ │ ├── 78.png │ │ │ ├── 79.png │ │ │ ├── 80.png │ │ │ ├── 81.png │ │ │ ├── 82.png │ │ │ ├── 83.png │ │ │ ├── 84.png │ │ │ ├── 85.png │ │ │ ├── 86.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 89.png │ │ │ ├── 90.png │ │ │ ├── 91.png │ │ │ ├── 92.png │ │ │ ├── 93.png │ │ │ ├── 94.png │ │ │ ├── 95.png │ │ │ ├── 96.png │ │ │ ├── 97.png │ │ │ ├── 98.png │ │ │ └── 99.png │ │ │ └── female │ │ │ ├── 1.png │ │ │ ├── 10.png │ │ │ ├── 11.png │ │ │ ├── 12.png │ │ │ ├── 13.png │ │ │ ├── 14.png │ │ │ ├── 15.png │ │ │ ├── 16.png │ │ │ ├── 17.png │ │ │ ├── 18.png │ │ │ ├── 19.png │ │ │ ├── 2.png │ │ │ ├── 20.png │ │ │ ├── 21.png │ │ │ ├── 22.png │ │ │ ├── 23.png │ │ │ ├── 24.png │ │ │ ├── 25.png │ │ │ ├── 26.png │ │ │ ├── 27.png │ │ │ ├── 28.png │ │ │ ├── 29.png │ │ │ ├── 3.png │ │ │ ├── 30.png │ │ │ ├── 31.png │ │ │ ├── 32.png │ │ │ ├── 33.png │ │ │ ├── 34.png │ │ │ ├── 35.png │ │ │ ├── 36.png │ │ │ ├── 37.png │ │ │ ├── 38.png │ │ │ ├── 39.png │ │ │ ├── 4.png │ │ │ ├── 40.png │ │ │ ├── 41.png │ │ │ ├── 42.png │ │ │ ├── 43.png │ │ │ ├── 44.png │ │ │ ├── 45.png │ │ │ ├── 46.png │ │ │ ├── 47.png │ │ │ ├── 48.png │ │ │ ├── 49.png │ │ │ ├── 5.png │ │ │ ├── 50.png │ │ │ ├── 51.png │ │ │ ├── 52.png │ │ │ ├── 53.png │ │ │ ├── 54.png │ │ │ ├── 55.png │ │ │ ├── 56.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 59.png │ │ │ ├── 6.png │ │ │ ├── 60.png │ │ │ ├── 61.png │ │ │ ├── 62.png │ │ │ ├── 63.png │ │ │ ├── 64.png │ │ │ ├── 65.png │ │ │ ├── 66.png │ │ │ ├── 67.png │ │ │ ├── 68.png │ │ │ ├── 69.png │ │ │ ├── 7.png │ │ │ ├── 70.png │ │ │ ├── 71.png │ │ │ ├── 72.png │ │ │ ├── 73.png │ │ │ ├── 74.png │ │ │ ├── 75.png │ │ │ ├── 76.png │ │ │ ├── 77.png │ │ │ ├── 78.png │ │ │ ├── 79.png │ │ │ ├── 8.png │ │ │ ├── 80.png │ │ │ ├── 81.png │ │ │ ├── 82.png │ │ │ ├── 83.png │ │ │ ├── 84.png │ │ │ ├── 85.png │ │ │ ├── 86.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 89.png │ │ │ ├── 9.png │ │ │ ├── 90.png │ │ │ ├── 91.png │ │ │ ├── 92.png │ │ │ ├── 93.png │ │ │ ├── 94.png │ │ │ ├── 95.png │ │ │ ├── 96.png │ │ │ ├── 97.png │ │ │ ├── 98.png │ │ │ ├── 99.png │ │ │ ├── 100.png │ │ │ ├── 101.png │ │ │ ├── 102.png │ │ │ ├── 103.png │ │ │ ├── 104.png │ │ │ ├── 105.png │ │ │ ├── 106.png │ │ │ ├── 107.png │ │ │ ├── 108.png │ │ │ ├── 109.png │ │ │ ├── 110.png │ │ │ ├── 111.png │ │ │ ├── 112.png │ │ │ ├── 113.png │ │ │ └── 114.png │ └── vendor │ │ ├── themes │ │ └── default │ │ │ └── assets │ │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.otf │ │ │ ├── icons.ttf │ │ │ ├── icons.woff │ │ │ └── icons.woff2 │ │ │ └── images │ │ │ └── flags.png │ │ └── phoenix_html.js └── repo │ └── migrations │ ├── 20160802005812_drop_login_tokens.exs │ ├── 20170113203212_remove_phase_from_project.exs │ ├── 20160809165928_rename_project_key_to_code.exs │ ├── 20160805154254_add_image_url_to_user.exs │ ├── 20160809001359_event_datetime_to_date.exs │ ├── 20160813010308_add_owner_to_deal.exs │ ├── 20160730111720_add_description_to_deal.exs │ ├── 20160803140627_add_user_id_to_events.exs │ ├── 20160806132149_add_image_url_to_contact.exs │ ├── 20160803134116_add_owner_id_to_account.exs │ ├── 20160803134825_add_user_id_to_reminders.exs │ ├── 20160904040426_add_active_to_workflow.exs │ ├── 20160715181420_create_account.exs │ ├── 20160716112506_add_color_to_account_statuses.exs │ ├── 20160812142212_deal_closing_date_as_date_only.exs │ ├── 20160806125103_remove_gender_add_title.exs │ ├── 20160822234907_add_private_field_to_events.exs │ ├── 20160715192600_add_event_to_account_reference.exs │ ├── 20160715194106_add_deal_to_account_reference.exs │ ├── 20160715191639_add_address_to_account_reference.exs │ ├── 20160715192416_add_contact_to_account_reference.exs │ ├── 20160830202441_add_date_range_to_projects.exs │ ├── 20160717113835_rename_description_to_key.exs │ ├── 20160806105135_remove_extended_address.exs │ ├── 20160715183744_create_event.exs │ ├── 20160715201102_create_deal_tag.exs │ ├── 20160715201039_create_event_tag.exs │ ├── 20160715201112_create_contact_tag.exs │ ├── 20160831190037_add_active_to_workflow_instance.exs │ ├── 20160715201028_create_account_tag.exs │ ├── 20160715193618_create_reminder.exs │ ├── 20160828153927_add_uploader_to_attachments.exs │ ├── 20160830202000_add_estimates_to_projects.exs │ ├── 20160825191652_add_use_distinct_address_for_shipping_in_account.exs │ ├── 20160815122355_alter_type_of_deal_expected_value_and_closed_value.exs │ ├── 20161223143603_reverse_project_phase_link.exs │ ├── 20160715202922_create_deal_stage.exs │ ├── 20160715185227_create_deal.exs │ ├── 20160803160512_drop_name_columns.exs │ ├── 20160822183017_use_clear_text_email_instead_of_hash.exs │ ├── 20160716112047_rename_join_tables.exs │ ├── 20160806103735_replace_varchar_by_text.exs │ ├── 20160819125916_add_meta_data_on_timesheet_status.exs │ ├── 20160903213152_add_owning_entity_fields_to_activity.exs │ ├── 20160805015529_reverse-account-address-assoc.exs │ ├── 20160822182428_add_email_notification_fields.exs │ ├── 20160715194427_create_account_status.exs │ ├── 20160830151030_remove_foreign_key_columns_from_attachments.exs │ ├── 20170104214855_change_field_presentation_order_from_index_to_constraint.exs │ ├── 20160715182822_create_contact.exs │ ├── 20160903104030_add_trigger_status_field.exs │ ├── 20160919112240_change_section_presentation_order_inder_from_index_to_constraint.exs │ ├── 20160807215557_add_activity_stream.exs │ ├── 20160715181814_create_address.exs │ ├── 20160913110611_convert_workflow_instace_state_presentation_order_index_from_index_to_constraint.exs │ ├── 20160909140134_add_active_to_states_sections_and_fields.exs │ ├── 20160803141343_add_projects_to_accounts.exs │ ├── 20160830203216_create_project_phase.exs │ ├── 20160809171552_create_project_tag.exs │ ├── 20160716113057_add_lock_columns.exs │ ├── 20160902133744_create_trigger.exs │ ├── 20160827165311_add_attachment_tables.exs │ ├── 20160715201917_create_tag_join_tables.exs │ ├── 20160716111057_add_active_field_to_all.exs │ ├── 20160730124546_create_user_table.exs │ └── 20160813142546_create_timesheet.exs ├── release.sh ├── .gitignore ├── config ├── test.exs ├── dev.exs └── config.exs ├── package.json ├── LICENSE └── rel └── config.exs /web/templates/session/email_sent.html.eex: -------------------------------------------------------------------------------- 1 | We've sent you an email! 2 | -------------------------------------------------------------------------------- /lib/carbon/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo do 2 | use Ecto.Repo, otp_app: :carbon 3 | end 4 | -------------------------------------------------------------------------------- /web/views/email_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EmailView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.PageView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.LayoutView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.SessionView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/tag_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TagView do 2 | use Carbon.Web, :view 3 | 4 | end 5 | -------------------------------------------------------------------------------- /web/views/reminder_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ReminderView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(Carbon.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /test/doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DocTest do 2 | use ExUnit.Case 3 | doctest Carbon.Duration 4 | end 5 | -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.PageViewTest do 2 | use Carbon.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/views/workflow/state_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.StateView do 2 | use Carbon.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.LayoutViewTest do 2 | use Carbon.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /priv/static/images/avatars/male/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/1.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/2.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/3.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/4.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/5.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/6.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/7.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/8.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/9.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/1.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/10.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/11.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/12.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/13.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/14.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/15.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/16.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/17.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/18.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/19.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/2.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/20.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/21.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/22.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/23.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/24.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/25.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/26.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/27.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/28.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/29.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/3.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/30.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/31.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/32.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/33.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/34.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/35.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/36.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/37.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/38.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/39.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/4.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/40.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/41.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/42.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/43.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/44.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/45.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/46.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/47.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/48.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/49.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/5.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/50.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/51.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/52.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/53.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/54.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/55.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/56.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/57.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/58.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/59.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/6.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/60.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/61.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/62.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/63.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/64.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/65.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/66.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/67.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/68.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/69.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/7.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/70.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/71.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/72.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/73.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/74.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/75.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/76.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/77.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/78.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/79.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/8.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/80.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/81.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/82.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/83.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/83.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/84.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/85.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/86.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/86.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/87.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/88.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/89.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/9.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/90.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/91.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/91.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/92.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/93.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/94.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/95.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/96.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/97.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/98.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/99.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/10.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/100.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/101.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/102.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/103.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/104.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/105.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/106.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/107.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/107.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/108.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/109.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/11.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/110.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/111.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/112.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/113.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/114.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/115.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/116.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/116.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/117.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/117.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/118.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/119.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/119.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/12.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/120.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/121.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/122.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/122.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/123.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/123.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/124.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/125.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/126.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/126.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/127.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/127.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/128.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/129.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/13.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/14.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/15.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/16.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/17.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/18.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/19.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/20.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/21.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/22.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/23.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/24.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/25.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/26.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/27.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/28.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/29.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/30.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/31.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/32.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/33.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/34.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/35.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/36.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/37.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/38.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/39.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/40.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/41.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/42.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/43.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/44.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/45.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/46.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/47.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/48.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/49.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/50.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/51.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/52.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/53.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/54.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/55.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/56.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/57.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/58.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/59.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/60.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/61.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/62.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/63.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/64.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/65.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/66.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/67.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/68.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/69.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/70.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/71.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/72.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/73.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/74.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/75.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/76.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/77.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/78.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/79.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/80.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/81.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/82.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/83.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/83.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/84.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/85.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/86.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/86.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/87.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/88.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/89.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/90.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/91.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/91.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/92.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/93.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/94.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/95.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/96.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/97.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/98.png -------------------------------------------------------------------------------- /priv/static/images/avatars/male/99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/male/99.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/100.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/101.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/102.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/103.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/104.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/105.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/106.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/107.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/107.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/108.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/109.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/110.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/111.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/112.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/113.png -------------------------------------------------------------------------------- /priv/static/images/avatars/female/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/images/avatars/female/114.png -------------------------------------------------------------------------------- /web/views/project_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ProjectView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers 4 | end 5 | 6 | -------------------------------------------------------------------------------- /web/views/workflow/field_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.FieldView do 2 | use Carbon.Web, :view 3 | alias Carbon.SupportedEnums 4 | 5 | end 6 | -------------------------------------------------------------------------------- /web/views/contact_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ContactView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [ contact_tags_select: 0 ] 4 | end 5 | -------------------------------------------------------------------------------- /web/views/event_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EventView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [event_tags_select: 0] 4 | 5 | end 6 | -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/images/flags.png -------------------------------------------------------------------------------- /priv/static/vendor/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code3-coop/carbon/HEAD/priv/static/vendor/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /web/views/deal_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [humanize: 2, probability_color: 1, deal_tags_select: 0] 4 | 5 | end 6 | -------------------------------------------------------------------------------- /web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.UserView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [humanize: 2, probability_color: 1] 4 | import Carbon.PaginatorHelper 5 | 6 | end 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160802005812_drop_login_tokens.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.DropLoginTokens do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table :login_tokens 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.PageController do 2 | use Carbon.Web, :controller 3 | 4 | def index(conn, _params) do 5 | redirect conn, to: account_path(conn, :index) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170113203212_remove_phase_from_project.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RemovePhaseFromProject do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop table :project_phases 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/templates/email/notification.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% reminder_count = length @reminders %> 4 | <%= reminder_count %> new notitication<%= if reminder_count > 1, do: "s" %> from Carbon 5 | 6 | 7 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160809165928_rename_project_key_to_code.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RenameProjectKeyToCode do 2 | use Ecto.Migration 3 | 4 | def change do 5 | rename table(:projects), :key, to: :code 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/templates/session/register_new_user.html.eex: -------------------------------------------------------------------------------- 1 |

2 | You don't have an account 3 |
4 | Can't create new user ATM 5 |

6 | <%= link "Wrong email ?", to: session_path(@conn, :index) %> 7 | 8 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160805154254_add_image_url_to_user.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddImageUrlToUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :users do 6 | add :image_url, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160809001359_event_datetime_to_date.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.EventDatetimeToDate do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :events do 6 | modify :date, :date 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160813010308_add_owner_to_deal.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddOwnerToDeal do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :deals do 6 | add :owner_id, references(:users) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160730111720_add_description_to_deal.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddDescriptionToDeal do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :deals do 6 | add :description, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160803140627_add_user_id_to_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddUserIdToEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :events do 6 | add :user_id, references(:users) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160806132149_add_image_url_to_contact.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddImageUrlToContact do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :contacts do 6 | add :image_url, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160803134116_add_owner_id_to_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddOwnerIdToAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :accounts do 6 | add :owner_id, references(:users) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160803134825_add_user_id_to_reminders.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddUserIdToReminders do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :reminders do 6 | add :user_id, references(:users) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160904040426_add_active_to_workflow.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddActiveToWorkflow do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :workflows do 6 | add :active, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715181420_create_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:accounts) do 6 | add :name, :string 7 | 8 | timestamps() 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160716112506_add_color_to_account_statuses.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddColorToAccountStatuses do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:account_statuses) do 6 | add :color, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160812142212_deal_closing_date_as_date_only.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.DealClosingDateAsDateOnly do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :deals do 6 | modify :closing_date, :date 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160806125103_remove_gender_add_title.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RemoveGenderAddTitle do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :contacts do 6 | remove :gender 7 | add :title, :string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160822234907_add_private_field_to_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddPrivateFieldToEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :events do 6 | add :private, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715192600_add_event_to_account_reference.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddEventToAccountReference do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:events) do 6 | add :account_id, references(:accounts) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715194106_add_deal_to_account_reference.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddDealToAccountReference do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:deals) do 6 | add :account_id, references(:accounts) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715191639_add_address_to_account_reference.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddAddressToAccountReference do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:addresses) do 6 | add :account_id, references(:accounts) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715192416_add_contact_to_account_reference.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddContactToAccountReference do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:contacts) do 6 | add :account_id, references(:accounts) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160830202441_add_date_range_to_projects.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddDateRangeToProjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :projects do 6 | add :start_date, :date 7 | add :end_date, :date 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160717113835_rename_description_to_key.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RenameDescriptionToKey do 2 | use Ecto.Migration 3 | 4 | def change do 5 | rename table(:account_statuses), :description, to: :key 6 | rename table(:deal_stages), :description, to: :key 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160806105135_remove_extended_address.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RemoveExtendedAddress do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :addresses do 6 | remove :extended_address 7 | remove :post_office_box 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715183744_create_event.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateEvent do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:events) do 6 | add :description, :string 7 | add :date, :datetime 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715201102_create_deal_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateDealTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:deal_tags) do 6 | add :description, :string 7 | add :color, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/carbon/release_tasks.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Release.Tasks do 2 | def migrate do 3 | {:ok, _} = Application.ensure_all_started(:carbon) 4 | 5 | path = Application.app_dir(:carbon, "priv/repo/migrations") 6 | 7 | Ecto.Migrator.run(Carbon.Repo, path, :up, all: true) 8 | 9 | :init.stop() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715201039_create_event_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateEventTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:event_tags) do 6 | add :description, :string 7 | add :color, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715201112_create_contact_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateContactTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:contact_tags) do 6 | add :description, :string 7 | add :color, :string 8 | timestamps() 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160831190037_add_active_to_workflow_instance.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddActiveToWorkflowInstance do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:workflow_instances) do 6 | add :active, :boolean, default: true, null: false 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715201028_create_account_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateAccountTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:account_tags) do 6 | add :description, :string 7 | add :color, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /web/templates/account/_address_show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= if @address do %> 3 |
4 | <%= @address.street_address %>
5 | <%= @address.locality %> <%= @address.region %> 6 | <%= @address.postal_code %>
7 | <%= @address.country_name %> 8 |
9 | <% end %> 10 |
11 | 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715193618_create_reminder.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateReminder do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:reminders) do 6 | add :date, :datetime 7 | add :event_id, references(:events) 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160828153927_add_uploader_to_attachments.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddUploaderToAttachments do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :attachments do 6 | add :user_id, references(:users) 7 | add :private, :boolean, default: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160830202000_add_estimates_to_projects.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddEstimatesToProjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :projects do 6 | add :estimate_unit, :string 7 | add :estimate_min, :float 8 | add :estimate_max, :float 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160825191652_add_use_distinct_address_for_shipping_in_account.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddUseDistinctAddressForShippingInAccount do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :accounts do 6 | add :use_distinct_address_for_shipping, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160815122355_alter_type_of_deal_expected_value_and_closed_value.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AlterTypeOfDealExpectedValueAndClosedValue do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :deals do 6 | modify :closed_value, :integer 7 | modify :expected_value, :integer 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20161223143603_reverse_project_phase_link.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.ReverseProjectPhaseLink do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:project_phases) do 6 | add :project_id, references(:projects) 7 | end 8 | alter table(:projects) do 9 | remove :phase_id 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715202922_create_deal_stage.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateDealStage do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:deal_stages) do 6 | add :description, :string 7 | add :color, :string 8 | add :active, :boolean, default: true, null: false 9 | 10 | timestamps() 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/models/workflow/instance_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.InstanceTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.Instance 5 | 6 | @valid_attrs %{lock_version: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Instance.changeset(%Instance{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715185227_create_deal.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateDeal do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:deals) do 6 | add :expected_value, :float 7 | add :probability, :integer 8 | add :closing_date, :datetime 9 | add :closed_value, :float 10 | 11 | timestamps() 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /web/controllers/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ControllerUtils do 2 | import Ecto.Query, only: [from: 2] 3 | alias Carbon.Repo 4 | 5 | def get_tags_from(_module, %{"tags_id" => ""}), do: [] 6 | def get_tags_from(module, %{"tags_id" => tags_id_param}) do 7 | ids = tags_id_param |> String.split(~r{\s*,\s*}) |> Enum.map(&String.to_integer/1) 8 | Repo.all(from t in module, where: t.id in ^ids) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160803160512_drop_name_columns.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.DropNameColumns do 2 | use Ecto.Migration 3 | 4 | def change do 5 | rename table(:contacts), :name, to: :full_name 6 | rename table(:users), :name, to: :full_name 7 | alter table :contacts do 8 | remove :given_name 9 | remove :additional_name 10 | remove :family_name 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160822183017_use_clear_text_email_instead_of_hash.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.UseClearTextEmailInsteadOfHash do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop index(:users, [:email_hash]) 6 | 7 | alter table :users do 8 | remove :email_hash 9 | add :email, :text, null: false 10 | end 11 | 12 | create unique_index(:users, [:email]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160716112047_rename_join_tables.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RenameJoinTables do 2 | use Ecto.Migration 3 | 4 | def change do 5 | rename table(:accounts_tags), to: table(:j_accounts_tags) 6 | rename table(:contacts_tags), to: table(:j_contacts_tags) 7 | rename table(:deals_tags), to: table(:j_deals_tags) 8 | rename table(:events_tags), to: table(:j_events_tags) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | echo Running release 2 | 3 | echo Stopping current instance 4 | _build/prod/rel/carbon/bin/carbon stop 5 | 6 | echo Updating dependencies 7 | mix deps.get 8 | 9 | echo Building release 10 | MIX_ENV=prod mix release --env=prod 11 | 12 | echo Running migrations 13 | _build/prod/rel/carbon/bin/carbon command Elixir.Carbon.Release.Tasks migrate 14 | 15 | echo Starting app 16 | _build/prod/rel/carbon/bin/carbon start 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160806103735_replace_varchar_by_text.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.ReplaceVarcharByText do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :events do 6 | modify :description, :text 7 | end 8 | alter table :deals do 9 | modify :description, :text 10 | end 11 | alter table :projects do 12 | modify :description, :text 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160819125916_add_meta_data_on_timesheet_status.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddMetaDataOnTimesheetStatus do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :timesheet_statuses do 6 | add :editable_by_owner?, :boolean, default: false 7 | add :editable_by_manager?, :boolean, default: false 8 | add :presentation_order, :integer 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160903213152_add_owning_entity_fields_to_activity.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddOwningEntityFieldsToActivity do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :activities do 6 | remove :account_id 7 | add :owning_entity_name, :string 8 | add :owning_entity_id, :integer 9 | end 10 | index :activities, [:owning_entity_name, :owning_entity_id] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160805015529_reverse-account-address-assoc.exs: -------------------------------------------------------------------------------- 1 | defmodule :"Elixir.Carbon.Repo.Migrations.Reverse-account-address-assoc" do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :accounts do 6 | add :billing_address_id, references(:addresses) 7 | add :shipping_address_id, references(:addresses) 8 | end 9 | alter table :addresses do 10 | remove :account_id 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160822182428_add_email_notification_fields.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddEmailNotificationFields do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :users do 6 | add :send_email_reminders, :boolean, default: true 7 | end 8 | alter table :reminders do 9 | add :seen, :boolean, default: false 10 | add :sent_by_email, :boolean, default: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715194427_create_account_status.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateAccountStatus do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:account_statuses) do 6 | add :description, :string 7 | add :active, :boolean, default: true 8 | timestamps() 9 | end 10 | alter table(:accounts) do 11 | add :status_id, references(:account_statuses) 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160830151030_remove_foreign_key_columns_from_attachments.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.RemoveForeignKeyColumnsFromAttachments do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :attachments do 6 | remove :contact_id 7 | remove :deal_id 8 | remove :project_id 9 | remove :workflow_instance_id 10 | remove :event_id 11 | remove :timesheet_id 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ErrorView do 2 | use Carbon.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170104214855_change_field_presentation_order_from_index_to_constraint.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.ChangeFieldPresentationOrderFromIndexToConstraint do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop unique_index :workflow_fields, [ :section_id, :presentation_order_index ] 6 | Ecto.Adapters.SQL.query! Carbon.Repo, "ALTER TABLE public.workflow_fields ADD UNIQUE (presentation_order_index, section_id) DEFERRABLE;" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/models/address_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AddressTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Address 5 | 6 | @valid_attrs %{country_name: "some content", locality: "some content", postal_code: "some content", region: "some content", street_address: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Address.changeset(%Address{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /web/views/activity_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ActivityView do 2 | use Carbon.Web, :view 3 | 4 | def past_tense(verb) do 5 | verb <> if String.ends_with?(verb, "e"), do: "d", else: "ed" 6 | end 7 | 8 | def singular(noun) do 9 | if String.ends_with?(noun, "s"), do: String.slice(noun, 0..-2), else: noun 10 | end 11 | 12 | def for_humans(field) do 13 | field 14 | |> String.replace(~r/_id$/, "") 15 | |> String.replace(~r/_/, " ") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715182822_create_contact.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateContact do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:contacts) do 6 | add :name, :string 7 | add :given_name, :string 8 | add :additional_name, :string 9 | add :family_name, :string 10 | add :email, :string 11 | add :tel, :string 12 | add :gender, :integer 13 | 14 | timestamps() 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160903104030_add_trigger_status_field.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddTriggerStatusField do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :activities do 6 | add :trigger_status, :string, default: "pending" 7 | end 8 | rename table(:activities), :target_schema, to: :entity_name 9 | rename table(:activities), :target_id, to: :entity_id 10 | rename table(:activities), :target_value, to: :changes 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160919112240_change_section_presentation_order_inder_from_index_to_constraint.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.ChangeSectionPresentationOrderInderFromIndexToConstraint do 2 | use Ecto.Migration 3 | 4 | def change do 5 | drop unique_index :workflow_sections, [ :workflow_id, :presentation_order_index ] 6 | Ecto.Adapters.SQL.query! Carbon.Repo, "ALTER TABLE public.workflow_sections ADD UNIQUE (presentation_order_index, workflow_id) DEFERRABLE;" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # The config/prod.secret.exs file by default contains sensitive 11 | # data and you should not commit it into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets file as long as you replace its contents by environment 15 | # variables. 16 | /config/prod.secret.exs 17 | 18 | .mix_tasks 19 | /node_modules 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160807215557_add_activity_stream.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddActivityStream do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :activities do 6 | add :account_id, references(:accounts) 7 | add :user_id, references(:users) 8 | add :action, :string, null: false 9 | add :target_schema, :string, null: false 10 | add :target_id, :integer 11 | add :target_value, :text 12 | timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/workflow/value_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.ValueTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.Value 5 | 6 | @valid_attrs %{boolean_value: true, date_value: %{day: 17, month: 4, year: 2010}, float_value: "120.5", integer_value: 42, lock_version: 42, string_value: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Value.changeset(%Value{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715181814_create_address.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateAddress do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:addresses) do 6 | add :street_address, :string 7 | add :extended_address, :string 8 | add :post_office_box, :string 9 | add :locality, :string 10 | add :region, :string 11 | add :postal_code, :string 12 | add :country_name, :string 13 | 14 | timestamps() 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/deal_stage.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealStage do 2 | use Carbon.Web, :model 3 | 4 | schema "deal_stages" do 5 | field :key, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:key, :color, :active]) 18 | |> validate_required([:key]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.UserTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.User 5 | 6 | @valid_attrs %{"full_name" => "Joe", "handle" => "joe"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/account_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Account 5 | 6 | @valid_attrs %{name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Account.changeset(%Account{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Account.changeset(%Account{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160913110611_convert_workflow_instace_state_presentation_order_index_from_index_to_constraint.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.ConvertWorkflowInstaceStatePresentationOrderIndexFromIndexToconstraint do 2 | use Ecto.Migration 3 | 4 | 5 | def change do 6 | drop unique_index :workflow_states, [ :workflow_id, :presentation_order_index ] 7 | Ecto.Adapters.SQL.query! Carbon.Repo, "ALTER TABLE public.workflow_states ADD UNIQUE (presentation_order_index, workflow_id) DEFERRABLE;" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /web/models/account_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountStatus do 2 | use Carbon.Web, :model 3 | 4 | schema "account_statuses" do 5 | field :key, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:key, :color, :active]) 18 | |> validate_required([:key]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /web/views/timesheet_entry_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TimesheetEntryView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [timehseet_entry_tags_select: 0] 4 | import Ecto.Query, only: [from: 2] 5 | 6 | 7 | def project_select do 8 | query = from p in Carbon.Project, 9 | where: p.active == true 10 | Carbon.Repo.all(query) 11 | end 12 | 13 | def account_select do 14 | query = from a in Carbon.Account, 15 | where: a.active == true 16 | Carbon.Repo.all(query) 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /web/views/workflow/section_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.SectionView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers 4 | 5 | def field_type_select do 6 | %{ 7 | "Long text": "long_text", 8 | "Short text": "text", 9 | "Integer": "integer", 10 | "Money amount": "currency", 11 | "Decimal": "decimal", 12 | "Date": "date", 13 | "Enumaration": "enum", 14 | "Reference": "reference", 15 | "Yes/No": "boolean", 16 | } 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160909140134_add_active_to_states_sections_and_fields.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddActiveToStatesSectionsAndFields do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table :workflow_states do 6 | add :active, :boolean, default: true 7 | end 8 | 9 | alter table :workflow_sections do 10 | add :active, :boolean, default: true 11 | end 12 | 13 | alter table :workflow_fields do 14 | add :active, :boolean, default: true 15 | end 16 | 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/role_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.RoleTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Role 5 | 6 | @valid_attrs %{"key" => "ADMIN", "description" => "system administrator"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Role.changeset(%Role{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Role.changeset(%Role{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/deal_tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealTagTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.DealTag 5 | 6 | @valid_attrs %{description: "some content", color: "red"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = DealTag.changeset(%DealTag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = DealTag.changeset(%DealTag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160803141343_add_projects_to_accounts.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddProjectsToAccounts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :projects do 6 | add :lock_version, :integer, default: 1, null: false 7 | add :account_id, references(:accounts) 8 | add :key, :string 9 | add :description, :string 10 | add :active, :boolean, default: true, null: false 11 | timestamps 12 | end 13 | 14 | create unique_index :projects, [:id, :key] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160830203216_create_project_phase.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateProject.Phase do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :project_phases do 6 | add :name, :string 7 | add :icon_name, :string 8 | add :color, :string 9 | add :presentation_order_index, :integer 10 | add :active, :boolean, default: true 11 | timestamps 12 | end 13 | 14 | alter table :projects do 15 | add :phase_id, references(:project_phases) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160809171552_create_project_tag.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateProjectTag do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :project_tags do 6 | add :description, :string 7 | add :color, :string 8 | add :active, :boolean, default: true, null: false 9 | timestamps 10 | end 11 | create table(:j_projects_tags, primary_key: false) do 12 | add :project_id, references(:projects) 13 | add :project_tag_id, references(:project_tags) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/models/event_tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EventTagTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.EventTag 5 | 6 | @valid_attrs %{description: "some content", color: "red"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = EventTag.changeset(%EventTag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = EventTag.changeset(%EventTag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/workflow/enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.EnumTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.Enum 5 | 6 | @valid_attrs %{name: "some content", presentation_order_index: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Enum.changeset(%Enum{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Enum.changeset(%Enum{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/workflow_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.WorkflowTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow 5 | 6 | @valid_attrs %{description: "some content", name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Workflow.changeset(%Workflow{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Workflow.changeset(%Workflow{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/account_status_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountStatusTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.AccountStatus 5 | 6 | @valid_attrs %{key: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = AccountStatus.changeset(%AccountStatus{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = AccountStatus.changeset(%AccountStatus{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/account_tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountTagTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.AccountTag 5 | 6 | @valid_attrs %{description: "some content", color: "red"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = AccountTag.changeset(%AccountTag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = AccountTag.changeset(%AccountTag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/contact_tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ContactTagTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.ContactTag 5 | 6 | @valid_attrs %{description: "some content", color: "red"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = ContactTag.changeset(%ContactTag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = ContactTag.changeset(%ContactTag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/deal_stage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealStageTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.DealStage 5 | 6 | @valid_attrs %{active: true, color: "some content", key: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = DealStage.changeset(%DealStage{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = DealStage.changeset(%DealStage{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/reminder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ReminderTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Reminder 5 | 6 | @valid_attrs %{date: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Reminder.changeset(%Reminder{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Reminder.changeset(%Reminder{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/project_tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ProjectTagTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.ProjectTag 5 | 6 | @valid_attrs %{active: true, color: "red", description: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = ProjectTag.changeset(%ProjectTag{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = ProjectTag.changeset(%ProjectTag{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/role.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Role do 2 | use Carbon.Web, :model 3 | 4 | schema "roles" do 5 | field :lock_version, :integer, default: 1 6 | field :active, :boolean, default: true 7 | 8 | field :key, :string 9 | field :description, :string 10 | 11 | timestamps 12 | end 13 | 14 | @doc """ 15 | Builds a changeset based on the `struct` and `params`. 16 | """ 17 | def changeset(role, params \\ %{}) do 18 | role 19 | |> cast(params, [:key, :description, :active]) 20 | |> validate_required([:key, :description]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/models/event_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EventTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Event 5 | 6 | @valid_attrs %{date: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}, description: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Event.changeset(%Event{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Event.changeset(%Event{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/workflow/enum.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.Enum do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_field_enums" do 5 | field :name, :string 6 | field :presentation_order_index, :integer, default: 0 7 | 8 | belongs_to :field, Carbon.Workflow.Field 9 | end 10 | 11 | @doc """ 12 | Builds a changeset based on the `struct` and `params`. 13 | """ 14 | def changeset(struct, params \\ %{}) do 15 | struct 16 | |> cast(params, [:name, :presentation_order_index]) 17 | |> validate_required([:name, :presentation_order_index]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/contact_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ContactTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Contact 5 | 6 | @valid_attrs %{email: "some content", full_name: "some content", tel: "some content", title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Contact.changeset(%Contact{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Contact.changeset(%Contact{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/deal_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealTag do 2 | use Carbon.Web, :model 3 | 4 | schema "deal_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/models/event_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EventTag do 2 | use Carbon.Web, :model 3 | 4 | schema "event_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160716113057_add_lock_columns.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddLockColumns do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :lock_version, :integer, default: 1, null: false 7 | end 8 | alter table(:addresses) do 9 | add :lock_version, :integer, default: 1, null: false 10 | end 11 | alter table(:contacts) do 12 | add :lock_version, :integer, default: 1, null: false 13 | end 14 | alter table(:deals) do 15 | add :lock_version, :integer, default: 1, null: false 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/workflow/section_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.SectionTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.Section 5 | 6 | @valid_attrs %{description: "some content", name: "some content", presentation_order_index: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Section.changeset(%Section{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Section.changeset(%Section{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/account_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountTag do 2 | use Carbon.Web, :model 3 | 4 | schema "account_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/models/contact_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ContactTag do 2 | use Carbon.Web, :model 3 | 4 | schema "contact_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/models/deal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.DealTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Deal 5 | 6 | @valid_attrs %{closed_value: "120", closing_date: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}, expected_value: "120", probability: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Deal.changeset(%Deal{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Deal.changeset(%Deal{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/workflow/state_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.StateTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.State 5 | 6 | @valid_attrs %{color: "some content", icon_name: "some content", name: "some content", presentation_order_index: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = State.changeset(%State{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = State.changeset(%State{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/project_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ProjectTag do 2 | use Carbon.Web, :model 3 | 4 | schema "project_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps() 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :carbon, Carbon.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :carbon, Carbon.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "carbon_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | 21 | 22 | config :carbon, :env, :test 23 | -------------------------------------------------------------------------------- /test/models/attachment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AttachmentTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Attachment 5 | 6 | @valid_attrs %{base64_content: "some content", description: "some content", mimetype: "some content", name: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Attachment.changeset(%Attachment{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Attachment.changeset(%Attachment{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/models/timesheet_entry_tag.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TimesheetEntryTag do 2 | use Carbon.Web, :model 3 | 4 | schema "timesheet_entry_tags" do 5 | field :description, :string 6 | field :color, :string 7 | field :active, :boolean, default: true 8 | 9 | timestamps 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:description, :color]) 18 | |> validate_required([:description, :color]) 19 | |> validate_inclusion(:color, Carbon.SupportedEnums.colors) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/controllers/activity_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ActivityController do 2 | use Carbon.Web, :controller 3 | 4 | alias Carbon.{ Account, Activity } 5 | 6 | def index(conn, %{"account_id" => account_id}) do 7 | activities = Repo.all from a in Activity, 8 | where: a.owning_entity_name == "account" and a.owning_entity_id == ^account_id, 9 | order_by: [ desc: a.inserted_at ], 10 | preload: [ :user ] 11 | 12 | account = Repo.get(Account, account_id) 13 | 14 | conn 15 | |> assign(:activities, activities) 16 | |> assign(:account, account) 17 | |> render("index.html") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/workflow/field_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.FieldTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Workflow.Field 5 | 6 | @valid_attrs %{description: "some content", entity_reference_name: "some content", name: "some content", presentation_order_index: 42, type: "text"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Field.changeset(%Field{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Field.changeset(%Field{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ChangesetView do 2 | use Carbon.Web, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `Carbon.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /web/models/timesheet_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TimesheetStatus do 2 | use Carbon.Web, :model 3 | 4 | schema "timesheet_statuses" do 5 | field :key, :string 6 | field :active, :boolean, default: false 7 | field :editable_by_owner?, :boolean, default: false 8 | field :editable_by_manager?, :boolean, default: false 9 | field :presentation_order, :integer 10 | timestamps 11 | end 12 | 13 | @doc """ 14 | Builds a changeset based on the `struct` and `params`. 15 | """ 16 | def changeset(struct, params \\ %{}) do 17 | struct 18 | |> cast(params, [:key, :active]) 19 | |> validate_required([:key]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ErrorViewTest do 2 | use Carbon.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(Carbon.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(Carbon.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(Carbon.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/models/timesheet_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TimesheetTest do 2 | use Carbon.ModelCase 3 | 4 | alias Carbon.Timesheet 5 | 6 | test "can extract total of billables and non billables minutes" do 7 | timesheet = %{ 8 | entries: [ 9 | %{duration_in_minutes: 10, billable: true}, 10 | %{duration_in_minutes: 10, billable: true}, 11 | %{duration_in_minutes: 1, billable: true}, 12 | %{duration_in_minutes: 22, billable: false}, 13 | %{duration_in_minutes: 2, billable: false}, 14 | ] 15 | } 16 | assert Timesheet.total_billables_and_non_billables(timesheet) == %{billables: 21, non_billables: 24} 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /web/models/workflow.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow do 2 | use Carbon.Web, :model 3 | 4 | schema "workflows" do 5 | field :name, :string 6 | field :description, :string 7 | field :active, :boolean, default: true 8 | 9 | has_many :sections, Carbon.Workflow.Section 10 | has_many :states, Carbon.Workflow.State 11 | has_many :instances, Carbon.Workflow.Instance 12 | 13 | timestamps 14 | end 15 | 16 | @doc """ 17 | Builds a changeset based on the `struct` and `params`. 18 | """ 19 | def changeset(struct, params \\ %{}) do 20 | struct 21 | |> cast(params, [:name, :description]) 22 | |> validate_required([:name, :description]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160902133744_create_trigger.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateTrigger do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:rules) do 6 | add :name, :string 7 | add :description, :string 8 | add :action, :string 9 | add :entity, :string 10 | add :field, :string 11 | add :value, :integer 12 | add :active, :boolean, default: true, null: false 13 | add :user_id, references(:users, on_delete: :nothing) 14 | add :workflow_id, references(:workflows, on_delete: :nothing) 15 | 16 | timestamps 17 | end 18 | create index(:rules, [:action, :entity, :field, :value]) 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /web/models/address.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Address do 2 | use Carbon.Web, :model 3 | 4 | schema "addresses" do 5 | field :lock_version, :integer, default: 1 6 | 7 | field :street_address, :string 8 | field :locality, :string 9 | field :region, :string 10 | field :postal_code, :string 11 | field :country_name, :string 12 | field :active, :boolean, default: true 13 | 14 | timestamps 15 | end 16 | 17 | @doc """ 18 | Builds a changeset based on the `struct` and `params`. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, [:street_address, :locality, :region, :postal_code, :country_name, :active]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carbon", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/code3-coop/carbon.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/code3-coop/carbon/issues" 20 | }, 21 | "homepage": "https://github.com/code3-coop/carbon#readme", 22 | "dependencies": { 23 | "jquery": "3.1.0", 24 | "semantic-ui-css": "2.2.2", 25 | "turbolinks": "^5.0.0", 26 | "sortablejs":"1.4.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160827165311_add_attachment_tables.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddAttachmentTables do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :attachments do 6 | add :name, :text 7 | add :description, :text 8 | add :mimetype, :text 9 | add :base64_content, :text 10 | add :account_id, references(:accounts) 11 | add :contact_id, references(:contacts) 12 | add :deal_id, references(:deals) 13 | add :event_id, references(:events) 14 | add :project_id, references(:projects) 15 | add :timesheet_id, references(:timesheets) 16 | add :workflow_instance_id, references(:workflow_instances) 17 | timestamps 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /web/models/workflow/state.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.State do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_states" do 5 | field :name, :string 6 | field :icon_name, :string 7 | field :color, :string 8 | field :presentation_order_index, :integer, default: 0 9 | field :active, :boolean, default: true 10 | 11 | belongs_to :workflow, Carbon.Workflow 12 | end 13 | 14 | @doc """ 15 | Builds a changeset based on the `struct` and `params`. 16 | """ 17 | def changeset(struct, params \\ %{}) do 18 | struct 19 | |> cast(params, [:name, :presentation_order_index, :icon_name, :color, :active]) 20 | |> validate_required([:name, :presentation_order_index, :icon_name, :color]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Carbon Client Relationship Management 2 | Copyright (C) 2016 CODE3 Coopérative de solidarité 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /web/templates/workflow/workflow/new.html.eex: -------------------------------------------------------------------------------- 1 |

New Workflow

2 | 3 | <%= form_for @changeset, workflow_path(@conn, :create), fn f -> %> 4 |
5 |
"> 6 | 10 | <%= text_input f, :name %> 11 |
12 | 13 |
"> 14 | 18 | <%= textarea f, :description %> 19 |
20 | 21 | <%= submit "Create workflow", class: "ui primary button"%> 22 | <%= link "Cancel", to: workflow_path(@conn, :index), class: "ui button"%> 23 | 24 | 25 | <% end %> 26 | -------------------------------------------------------------------------------- /test/views/account_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountViewTest do 2 | use Carbon.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | # import Phoenix.View 6 | 7 | test "events are grouped, filtered and sorted" do 8 | reference_date = Ecto.Date.from_erl {2016, 8, 15} 9 | all_events = for n <- -10..10 do 10 | # using description to test equality 11 | %Carbon.Event{description: "#{n}", date: Ecto.Date.from_erl({2016, 8, 15+n})} 12 | end 13 | 14 | { lt_events, gte_events } = Carbon.AccountView.events_summary(all_events, reference_date) 15 | 16 | assert ~w(-1 -2 -3 -4 -5) == Enum.map(lt_events, &(&1.description)) 17 | assert ~w(4 3 2 1 0) == Enum.map(gte_events, &(&1.description)) 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /web/templates/session/index.html.eex: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |

Log in

18 | <%= form_for @changeset, session_path(@conn, :create_and_send_session_link), fn f -> %> 19 |
20 |
21 | 22 | 23 | <%= text_input f, :email %> 24 | <%= submit"Login", class: "ui button primary"%> 25 | 26 |
27 | 28 |
29 | <% end %> 30 |
31 |
32 | -------------------------------------------------------------------------------- /web/models/workflow/section.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.Section do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_sections" do 5 | field :name, :string 6 | field :description, :string 7 | field :presentation_order_index, :integer, default: 0 8 | field :active, :boolean, default: true 9 | 10 | has_many :fields, Carbon.Workflow.Field 11 | belongs_to :workflow, Carbon.Workflow 12 | end 13 | 14 | @doc """ 15 | Builds a changeset based on the `struct` and `params`. 16 | """ 17 | def changeset(struct, params \\ %{}) do 18 | struct 19 | |> cast(params, [:name, :description, :presentation_order_index, :active]) 20 | |> validate_required([:name, :description, :presentation_order_index]) 21 | |> cast_assoc(:fields, required: false) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import Carbon.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :carbon 24 | end 25 | -------------------------------------------------------------------------------- /web/models/contact.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Contact do 2 | use Carbon.Web, :model 3 | 4 | schema "contacts" do 5 | field :lock_version, :integer, default: 1 6 | 7 | field :full_name, :string 8 | field :title, :string 9 | field :email, :string 10 | field :tel, :string 11 | field :image_url, :string 12 | field :active, :boolean, default: true 13 | 14 | belongs_to :account, Carbon.Account 15 | many_to_many :tags, Carbon.ContactTag, join_through: "j_contacts_tags", on_replace: :delete 16 | 17 | timestamps 18 | end 19 | 20 | @doc """ 21 | Builds a changeset based on the `struct` and `params`. 22 | """ 23 | def changeset(contact, params \\ %{}) do 24 | contact 25 | |> cast(params, [:full_name, :title, :email, :tel, :image_url, :active]) 26 | |> validate_required([:full_name]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160715201917_create_tag_join_tables.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateTagJoinTables do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:accounts_tags, primary_key: false) do 6 | add :account_id, references(:accounts) 7 | add :account_tag_id, references(:account_tags) 8 | end 9 | create table(:events_tags, primary_key: false) do 10 | add :event_id, references(:events) 11 | add :event_tag_id, references(:event_tags) 12 | end 13 | create table(:deals_tags, primary_key: false) do 14 | add :deal_id, references(:deals) 15 | add :deal_tag_id, references(:deal_tags) 16 | end 17 | create table(:contacts_tags, primary_key: false) do 18 | add :contact_id, references(:contacts) 19 | add :contact_tag_id, references(:contact_tags) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /web/models/attachment.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Attachment do 2 | use Carbon.Web, :model 3 | 4 | schema "attachments" do 5 | field :name, :string 6 | field :description, :string 7 | field :private, :boolean, default: false 8 | field :mimetype, :string 9 | field :base64_content, :string 10 | 11 | belongs_to :user, Carbon.User 12 | belongs_to :account, Carbon.Account 13 | 14 | timestamps 15 | end 16 | 17 | @doc """ 18 | Builds a changeset based on the `struct` and `params`. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, [:name, :description, :private, :mimetype, :base64_content]) 23 | |> validate_required([:name, :mimetype, :base64_content]) 24 | end 25 | def update_changeset(attachment, params) do 26 | attachment 27 | |> cast(params, [:description, :private]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /web/models/workflow/value.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.Value do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_field_values" do 5 | field :lock_version, :integer, default: 1 6 | 7 | field :string_value, :string 8 | field :integer_value, :integer 9 | field :float_value, :float 10 | field :date_value, Ecto.Date 11 | field :boolean_value, :boolean 12 | 13 | belongs_to :field, Carbon.Workflow.Field 14 | belongs_to :instance, Carbon.Workflow.Instance 15 | 16 | timestamps 17 | end 18 | 19 | @doc """ 20 | Builds a changeset based on the `struct` and `params`. 21 | """ 22 | def changeset(struct, params \\ %{}) do 23 | struct 24 | |> cast(params, [:lock_version, :string_value, :integer_value, :float_value, :date_value, :boolean_value]) 25 | |> validate_required([:lock_version]) 26 | |> optimistic_lock(:lock_version) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.User do 2 | use Carbon.Web, :model 3 | 4 | schema "users" do 5 | field :lock_version, :integer, default: 1 6 | field :active, :boolean, default: true 7 | 8 | field :handle, :string 9 | field :full_name, :string 10 | field :title, :string 11 | field :email, :string 12 | field :image_url, :string 13 | field :send_email_reminders, :boolean, default: true 14 | 15 | many_to_many :roles, Carbon.Role, join_through: "j_users_roles" 16 | 17 | timestamps 18 | end 19 | 20 | def changeset(user, params \\ %{}) do 21 | user 22 | |> cast(params, [:full_name, :handle, :title, :email, :image_url]) 23 | |> validate_required([:full_name, :handle]) 24 | |> unique_constraint(:handle) 25 | |> unique_constraint(:email) 26 | end 27 | 28 | def login_changeset(struct, params \\ %{}) do 29 | struct 30 | |> cast(params, [:email]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /web/views/workflow/workflow_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.WorkflowView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers 4 | # import Ecto.Query, only: [from: 2] 5 | 6 | @now Ecto.DateTime.from_erl(:calendar.local_time) 7 | 8 | @fake_workflows [ 9 | %{name: "Cloud shoveling", description: "The nice proces for looking really busy without actually solving anything for anyone", sections: 0..10, instances: 0..14, states: 0..8, updated_at: @now}, 10 | %{name: "Search of lost", description: "The fine art of losing thing and then spending minutes, hours, even days looking for your stuff.", sections: [], instances: 0..900, states: 0..4, updated_at: @now}, 11 | %{name: "Wine tasting", description: "One of the only thing that matters in life", sections: 0..900, instances: [], states: 0..90, updated_at: @now}, 12 | ] 13 | 14 | def random_fake_workflow do 15 | Enum.random(@fake_workflows) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /priv/static/vendor/phoenix_html.js: -------------------------------------------------------------------------------- 1 | function isLinkToSubmitParent(element) { 2 | var isLinkTag = element.tagName === 'A'; 3 | var shouldSubmitParent = element.getAttribute('data-submit') === 'parent'; 4 | 5 | return isLinkTag && shouldSubmitParent; 6 | } 7 | 8 | function didHandleSubmitLinkClick(element) { 9 | while(element && element.getAttribute) { 10 | if(isLinkToSubmitParent(element)) { 11 | var message = element.getAttribute('data-confirm'); 12 | if (message === null || confirm(message)) { 13 | element.parentNode.submit(); 14 | }; 15 | return true; 16 | } else { 17 | element = element.parentNode; 18 | } 19 | } 20 | return false; 21 | } 22 | 23 | // for links with HTTP methods other than GET 24 | window.addEventListener('click', function (event) { 25 | if(event.target && didHandleSubmitLinkClick(event.target)) { 26 | event.preventDefault(); 27 | return false; 28 | } 29 | }, false); 30 | -------------------------------------------------------------------------------- /lib/carbon/search_index.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.SearchIndex do 2 | use GenServer 3 | 4 | @five_seconds_in_millis 5 * 1000 5 | 6 | def start_link do 7 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | def refresh do 11 | GenServer.cast(__MODULE__, :refresh) 12 | end 13 | 14 | def init(:ok) do 15 | refresh 16 | { :ok, %{} } 17 | end 18 | 19 | def handle_cast(:refresh, state) do 20 | if state[:timer] do 21 | Process.cancel_timer(state[:timer]) 22 | end 23 | new_timer = Process.send_after(__MODULE__, :do_refresh, @five_seconds_in_millis) 24 | { :noreply, Map.put(state, :timer, new_timer) } 25 | end 26 | 27 | def handle_info(:do_refresh, state) do 28 | Task.yield_many [ 29 | Task.async(Ecto.Adapters.SQL, :query, [Carbon.Repo, "refresh materialized view search_index;"]), 30 | Task.async(Ecto.Adapters.SQL, :query, [Carbon.Repo, "refresh materialized view search_words;"]), 31 | ] 32 | { :noreply, state } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /web/templates/workflow/section/new.html.eex: -------------------------------------------------------------------------------- 1 |

2 | Create workflow section 3 |

4 | 5 | <%= form_for @changeset, workflow_section_path(@conn, :create, @conn.params["workflow_id"]), fn f -> %> 6 | 7 |
"> 8 |
9 |
Ruh roh!
10 |

Something went wrong. Please check the following errors.

11 |
12 | 13 | 14 |
"> 15 | 16 | <%= text_input f, :name %> 17 |
18 | 19 |
"> 20 | 21 | <%= textarea f, :description %> 22 |
23 | 24 | <%= submit "Create section", class: "ui button primary" %> 25 | <%= link "Cancel", to: workflow_path(@conn, :edit, @conn.params["workflow_id"]), class: "ui button" %> 26 | 27 |
28 | <% end %> 29 | -------------------------------------------------------------------------------- /web/models/rule.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Rule do 2 | use Carbon.Web, :model 3 | 4 | @allowed_entities %{ 5 | "account" => ~w(status_id owner_id), 6 | "timesheet" => ~w(status_id), 7 | } 8 | 9 | def allowed_entities, do: @allowed_entities 10 | 11 | schema "rules" do 12 | field :name, :string 13 | field :description, :string 14 | field :action, :string 15 | field :entity, :string 16 | field :field, :string 17 | field :value, :integer 18 | field :active, :boolean, default: true 19 | 20 | belongs_to :user, Carbon.User 21 | belongs_to :workflow, Carbon.Workflow 22 | 23 | timestamps 24 | end 25 | 26 | @doc """ 27 | Builds a changeset based on the `struct` and `params`. 28 | """ 29 | def changeset(struct, params \\ %{}) do 30 | struct 31 | |> cast(params, [:name, :description, :action, :entity, :field, :value, :active]) 32 | |> validate_required([:name, :description, :action, :entity]) 33 | |> validate_inclusion(:action, ~w(create remove update restore)) 34 | |> validate_inclusion(:entity, Map.keys(@allowed_entities)) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /web/templates/attachment/edit.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, account_attachment_path(@conn, :update, @account_id, @id), fn f -> %> 2 |
"> 3 |
4 |
Ruh roh!
5 |

Something went wrong. Please check the following errors.

6 |
7 | 8 |
"> 9 | 14 |
15 | <%= text_input f, :description %> 16 | 17 |
18 |
19 | 20 |
21 |
22 | <%= checkbox f, :private %> 23 | 24 |
25 |
26 | 27 |
28 | <%= submit "Update", class: "ui submit primary button" %> 29 | <%= link "cancel", to: "", class: "ui button" %> 30 |
31 | <% end %> 32 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias Carbon.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | 29 | # The default endpoint for testing 30 | @endpoint Carbon.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Carbon.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(Carbon.Repo, {:shared, self()}) 39 | end 40 | 41 | :ok 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/carbon/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Mailer do 2 | use Bamboo.Mailer, otp_app: :carbon 3 | import Bamboo.Email 4 | 5 | def send_login_link(email, token) do 6 | website_entry_point = Application.get_env(:carbon, Carbon.Mailer)[:website_entry_point] 7 | new_email 8 | |> to(email) 9 | |> from("login@carbon-app.com") 10 | |> subject("Your login link!") 11 | |> html_body(Phoenix.View.render_to_string(Carbon.EmailView, "login.html", %{token: token, website_entry_point: website_entry_point})) 12 | |> Carbon.Mailer.deliver_now 13 | end 14 | 15 | def send_notification(reminders_for_one_user) do 16 | try do 17 | to_email = hd(reminders_for_one_user).user.email 18 | reminder_count = length reminders_for_one_user 19 | 20 | new_email 21 | |> to(to_email) 22 | |> from("notifications@carbon-app.com") 23 | |> subject("#{reminder_count} new notitication#{if reminder_count > 1, do: "s"} from Carbon") 24 | |> html_body(Phoenix.View.render_to_string(Carbon.EmailView, "notification.html", %{reminders: reminders_for_one_user})) 25 | |> Carbon.Mailer.deliver_later 26 | 27 | :ok 28 | rescue 29 | _ -> :error 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/carbon.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(Carbon.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(Carbon.Endpoint, []), 15 | # Start your own worker by calling: Carbon.Worker.start_link(arg1, arg2, arg3) 16 | # worker(Carbon.Worker, [arg1, arg2, arg3]), 17 | worker(Carbon.SearchIndex, []), 18 | worker(Carbon.EmailNotificationSender, []), 19 | ] 20 | 21 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 22 | # for other strategies and supported options 23 | opts = [strategy: :one_for_one, name: Carbon.Supervisor] 24 | Supervisor.start_link(children, opts) 25 | end 26 | 27 | # Tell Phoenix to update the endpoint configuration 28 | # whenever the application is updated. 29 | def config_change(changed, _new, removed) do 30 | Carbon.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160716111057_add_active_field_to_all.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.AddActiveFieldToAll do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:accounts) do 6 | add :active, :boolean, default: true, null: false 7 | end 8 | alter table(:account_tags) do 9 | add :active, :boolean, default: true, null: false 10 | end 11 | alter table(:addresses) do 12 | add :active, :boolean, default: true, null: false 13 | end 14 | alter table(:contacts) do 15 | add :active, :boolean, default: true, null: false 16 | end 17 | alter table(:contact_tags) do 18 | add :active, :boolean, default: true, null: false 19 | end 20 | alter table(:deals) do 21 | add :active, :boolean, default: true, null: false 22 | end 23 | alter table(:deal_tags) do 24 | add :active, :boolean, default: true, null: false 25 | end 26 | alter table(:events) do 27 | add :active, :boolean, default: true, null: false 28 | end 29 | alter table(:event_tags) do 30 | add :active, :boolean, default: true, null: false 31 | end 32 | alter table(:reminders) do 33 | add :active, :boolean, default: true, null: false 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /web/models/workflow/instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.Instance do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_instances" do 5 | field :lock_version, :integer, default: 1 6 | field :active, :boolean, default: true 7 | 8 | belongs_to :workflow, Carbon.Workflow 9 | belongs_to :state, Carbon.Workflow.State 10 | 11 | has_many :values, Carbon.Workflow.Value 12 | 13 | timestamps 14 | end 15 | 16 | @doc """ 17 | Builds a changeset based on the `struct` and `params`. 18 | """ 19 | def changeset(struct, params \\ %{}) do 20 | struct 21 | |> cast(params, [:lock_version, :state_id, :workflow_id]) 22 | |> validate_required([:lock_version]) 23 | |> foreign_key_constraint(:state_id) 24 | |> foreign_key_constraint(:workflow_id) 25 | |> optimistic_lock(:lock_version) 26 | end 27 | 28 | def create_changeset(struct, params \\ %{}, values) do 29 | struct 30 | |> cast(params, [:lock_version, :state_id, :workflow_id]) 31 | |> validate_required([:lock_version]) 32 | |> foreign_key_constraint(:state_id) 33 | |> foreign_key_constraint(:workflow_id) 34 | |> put_assoc(:values, Enum.map(values, &Ecto.Changeset.change/1)) 35 | |> optimistic_lock(:lock_version) 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Releases.Config, 2 | # This sets the default release built by `mix release` 3 | default_release: :default, 4 | # This sets the default environment used by `mix release` 5 | default_environment: :dev 6 | 7 | # For a full list of config options for both releases 8 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 9 | 10 | 11 | # You may define one or more environments in this file, 12 | # an environment's settings will override those of a release 13 | # when building in that environment, this combination of release 14 | # and environment configuration is called a profile 15 | 16 | environment :dev do 17 | set dev_mode: true 18 | set include_erts: false 19 | set cookie: :"TGzOG[AXUAV82WSvZ*^f,o>[8[][]DCOK?WFmLAv>M_L@=~[dMS]%J3WSi.h6N8b" 20 | end 21 | 22 | environment :prod do 23 | set include_erts: true 24 | set include_src: false 25 | set cookie: :"43USzKkrs(*_t@mC(RojHp.Nru/i:ovXY`T!]OFZ3{Q)>s!*VjiAQ1^_ALn`cNIt" 26 | end 27 | 28 | # You may define one or more releases in this file. 29 | # If you have not set a default release, or selected one 30 | # when running `mix release`, the first release in the file 31 | # will be used by default 32 | 33 | release :carbon do 34 | set version: current_version(:carbon) 35 | end 36 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias Carbon.Repo 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | 28 | import Carbon.Router.Helpers 29 | 30 | # The default endpoint for testing 31 | @endpoint Carbon.Endpoint 32 | end 33 | end 34 | 35 | setup tags do 36 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Carbon.Repo) 37 | 38 | unless tags[:async] do 39 | Ecto.Adapters.SQL.Sandbox.mode(Carbon.Repo, {:shared, self()}) 40 | end 41 | 42 | {:ok, conn: Phoenix.ConnTest.build_conn()} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /web/models/workflow/field.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.Field do 2 | use Carbon.Web, :model 3 | 4 | schema "workflow_fields" do 5 | field :name, :string 6 | field :description, :string 7 | field :type, :string 8 | field :presentation_order_index, :integer, default: 0 9 | field :entity_reference_name, :string 10 | field :active, :boolean, default: true 11 | 12 | has_many :enums, Carbon.Workflow.Enum 13 | belongs_to :section, Carbon.Workflow.Section 14 | end 15 | 16 | def reference_user(field), do: reference_type field, "Carbon.User" 17 | def reference_account(field), do: reference_type field, "Carbon.Account" 18 | def reference_timesheet(field), do: reference_type field, "Carbon.Timesheet" 19 | 20 | defp reference_type(field, type) do 21 | field.type == "reference" && field.entity_reference_name == type 22 | end 23 | 24 | @doc """ 25 | Builds a changeset based on the `struct` and `params`. 26 | """ 27 | def changeset(struct, params \\ %{}) do 28 | struct 29 | |> cast(params, [:name, :description, :type, :entity_reference_name, :presentation_order_index]) 30 | |> validate_required([:name, :description, :type, :presentation_order_index]) 31 | |> validate_inclusion(:type, Carbon.SupportedEnums.field_types) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", Carbon.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # Carbon.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /web/controllers/paginator.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Paginator do 2 | import Ecto.Query, only: [from: 2] 3 | alias Carbon.Repo 4 | 5 | @default_page "1" 6 | @default_length "25" 7 | 8 | defstruct [:page, :length, :total_length, :number_of_pages, :data] 9 | 10 | def create(query, params, length \\ @default_length) 11 | def create(query, params, length) when is_integer(length) do 12 | create(query, params, Integer.to_string(length)) 13 | end 14 | def create(query, params, length) do 15 | page = Map.get(params, "page", @default_page) |> Integer.parse() |> elem(0) |> max(1) 16 | length = Map.get(params, "length", length) |> Integer.parse() |> elem(0) |> max(1) 17 | 18 | total_length = Repo.aggregate(query, :count, :id) 19 | 20 | new_query = from query, 21 | limit: ^length, 22 | offset: ^get_offset(page, length) 23 | 24 | %__MODULE__{ 25 | page: page, 26 | length: length, 27 | total_length: total_length, 28 | number_of_pages: number_of_pages(total_length, length), 29 | data: Repo.all(new_query) 30 | } 31 | end 32 | 33 | defp number_of_pages(total_length, length_per_page) do 34 | round(Float.ceil(total_length / length_per_page)) 35 | end 36 | 37 | defp get_offset(current_page, length_per_page) do 38 | (current_page - 1) * length_per_page 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lib/carbon/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :carbon 3 | 4 | socket "/socket", Carbon.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :carbon, gzip: false, 12 | only: ~w(css fonts images js vendor favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | # The session will be stored in the cookie and signed, 34 | # this means its contents can be read but not tampered with. 35 | # Set :encryption_salt if you would also like to encrypt it. 36 | plug Plug.Session, 37 | store: :cookie, 38 | key: "_carbon_key", 39 | signing_salt: "DQeqrPEb" 40 | 41 | plug Carbon.Router 42 | end 43 | -------------------------------------------------------------------------------- /web/models/project.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Project do 2 | use Carbon.Web, :model 3 | 4 | schema "projects" do 5 | field :lock_version, :integer, default: 1 6 | 7 | field :code, :string 8 | field :description, :string 9 | field :estimate_unit, :string, default: "CAD" 10 | field :estimate_min, :float 11 | field :estimate_max, :float 12 | field :start_date, Ecto.Date 13 | field :end_date, Ecto.Date 14 | 15 | field :active, :boolean, default: true 16 | 17 | belongs_to :account, Carbon.Account 18 | 19 | many_to_many :tags, Carbon.ProjectTag, join_through: "j_projects_tags", on_replace: :delete 20 | 21 | timestamps 22 | end 23 | 24 | @doc """ 25 | Builds a changeset based on the `struct` and `params`. 26 | """ 27 | def changeset(struct, params \\ %{}) do 28 | struct 29 | |> cast(params, [:code, :description, :active, :estimate_unit, :estimate_min, :estimate_max, :start_date, :end_date]) 30 | |> validate_required([:code]) 31 | |> validate_inclusion(:estimate_unit, ~w(hours CAD)) 32 | |> optimistic_lock(:lock_version) 33 | end 34 | 35 | def update_changeset(struct, params \\ %{}, tags) do 36 | changeset(struct, params) 37 | |> put_assoc(:tags, Enum.map(tags, &Ecto.Changeset.change/1)) 38 | end 39 | 40 | def archive_changeset(struct, params \\ %{}) do 41 | struct 42 | |> cast(params, [:active]) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /web/templates/attachment/new.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, account_attachment_path(@conn, :create, @account_id), [ multipart: true ], fn f -> %> 2 |
"> 3 |
4 |
Ruh roh!
5 |

Something went wrong. Please check the following errors.

6 |
7 | 8 |
9 | 10 |
11 | <%= file_input f, :file %> 12 |
13 |
14 |
15 | 16 |
"> 17 | 22 |
23 | <%= text_input f, :description %> 24 | 25 |
26 |
27 | 28 |
29 |
30 | <%= checkbox f, :private %> 31 | 32 |
33 |
34 | 35 |
36 | <%= submit "Upload", class: "ui submit primary button" %> 37 | <%= link "cancel", to: "", class: "ui button" %> 38 |
39 | <% end %> 40 | -------------------------------------------------------------------------------- /web/templates/tag/index.html.eex: -------------------------------------------------------------------------------- 1 | <%= if success_message = get_flash(@conn, :success) do %> 2 |
3 | 4 |
<%= success_message %>
5 | <%= if restore_link = get_flash(@conn, :restore_link) do %> 6 |
7 | Oops! That's not what I wanted to do. <%= link method: "put", to: restore_link, form: [class: "inline-form", style: "display:inline"] do %>Put it back!<% end %> 8 | <% end %> 9 |
10 | <% end %> 11 | 12 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @account_tags, occurrences: @account_tag_occurs, tagged: "account" %> 13 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @contact_tags, occurrences: @contact_tag_occurs, tagged: "contact" %> 14 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @deal_tags, occurrences: @deal_tag_occurs, tagged: "deal" %> 15 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @event_tags, occurrences: @event_tag_occurs, tagged: "event" %> 16 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @project_tags, occurrences: @project_tag_occurs, tagged: "project" %> 17 | <%= render Carbon.TagView, "_show_card.html", conn: @conn, tags: @timesheet_tags, occurrences: @timesheet_tag_occurs, tagged: "timesheet" %> 18 | 19 | 24 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160730124546_create_user_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateUserTable do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table :users do 6 | add :lock_version, :integer, default: 1, null: false 7 | 8 | add :handle, :string, null: false 9 | add :name, :string 10 | add :title, :string 11 | add :email_hash, :string, null: false 12 | 13 | add :active, :boolean, null: false, default: true 14 | 15 | timestamps 16 | end 17 | 18 | create unique_index(:users, [:handle]) 19 | create unique_index(:users, [:email_hash]) 20 | 21 | create table :roles do 22 | add :lock_version, :integer, default: 1, null: false 23 | 24 | add :key, :string, null: false 25 | add :description, :string 26 | 27 | add :active, :boolean, null: false, default: true 28 | 29 | timestamps 30 | end 31 | 32 | create unique_index(:roles, [:key]) 33 | 34 | create table(:j_users_roles, primary_key: false) do 35 | add :user_id, references(:users) 36 | add :role_id, references(:roles) 37 | end 38 | 39 | create unique_index(:j_users_roles, [:user_id, :role_id]) 40 | 41 | create table :login_tokens do 42 | add :user_id, references(:users) 43 | add :token, :string, null: false 44 | add :type, :string, null: false 45 | 46 | timestamps 47 | end 48 | 49 | create index(:login_tokens, [:token]) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /web/controllers/session_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.SessionPlug do 2 | import Plug.Conn 3 | alias Carbon.{User, Repo} 4 | 5 | @moduledoc """ 6 | Intercepts protected routes. 7 | 8 | Checks the connection for a web session or a cookie containing a long-lived 9 | session token. If the web session is present, pass-through. If the long-lived 10 | session token is still valid, assign the user to the web session and 11 | pass-through. Otherwise, halt and redirect to the login page. 12 | """ 13 | 14 | @one_week_in_sec 7*24*60*60 15 | 16 | def init(opts), do: opts 17 | 18 | def call(conn, _opts) do 19 | conn 20 | |> maybe_assign_user 21 | |> maybe_redirect_to_login_page 22 | end 23 | 24 | defp maybe_assign_user(conn) do 25 | case get_key(conn, :user_id) do 26 | nil -> conn 27 | user_id -> assign(conn, :current_user, Repo.get!(User, user_id)) 28 | end 29 | end 30 | 31 | defp maybe_redirect_to_login_page(%Plug.Conn{assigns: %{current_user: user}} = conn) when user != nil, do: conn 32 | defp maybe_redirect_to_login_page(conn) do 33 | conn 34 | |> Phoenix.Controller.put_flash(:error, "You must be logged in to access this page") 35 | |> Phoenix.Controller.redirect(to: Carbon.Router.Helpers.session_path(conn, :index)) 36 | |> halt() 37 | end 38 | 39 | defp get_key(conn, key) do 40 | if Application.get_env(:carbon, :env) == :test do 41 | conn.private[key] 42 | else 43 | get_session(conn, key) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :carbon, Carbon.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [] 15 | 16 | # Watch static and templates for browser reloading. 17 | config :carbon, Carbon.Endpoint, 18 | live_reload: [ 19 | patterns: [ 20 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 21 | ~r{priv/gettext/.*(po)$}, 22 | ~r{web/views/.*(ex)$}, 23 | ~r{web/templates/.*(eex)$} 24 | ] 25 | ] 26 | 27 | # Do not include metadata nor timestamps in development logs 28 | config :logger, :console, format: "[$level] $message\n" 29 | 30 | # Set a higher stacktrace during development. Avoid configuring such 31 | # in production as building large stacktraces may be expensive. 32 | config :phoenix, :stacktrace_depth, 20 33 | 34 | # Configure your database 35 | config :carbon, Carbon.Repo, 36 | adapter: Ecto.Adapters.Postgres, 37 | username: "postgres", 38 | password: "postgres", 39 | database: "carbon_dev", 40 | hostname: "localhost", 41 | pool_size: 10 42 | 43 | config :carbon, Carbon.Mailer, 44 | adapter: Bamboo.LocalAdapter, 45 | website_entry_point: "http://localhost:4000" 46 | 47 | config :carbon, :env, :dev 48 | -------------------------------------------------------------------------------- /web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error) 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({"is invalid", [type: :integer]}) do 21 | Gettext.dgettext(Carbon.Gettext, "errors", "must be an integer") 22 | end 23 | def translate_error({msg, opts}) do 24 | # Because error messages were defined within Ecto, we must 25 | # call the Gettext module passing our Gettext backend. We 26 | # also use the "errors" domain as translations are placed 27 | # in the errors.po file. 28 | # Ecto will pass the :count keyword if the error message is 29 | # meant to be pluralized. 30 | # On your own code and templates, depending on whether you 31 | # need the message to be pluralized or not, this could be 32 | # written simply as: 33 | # 34 | # dngettext "errors", "1 file", "%{count} files", count 35 | # dgettext "errors", "is invalid" 36 | # 37 | if count = opts[:count] do 38 | Gettext.dngettext(Carbon.Gettext, "errors", msg, msg, count, opts) 39 | else 40 | Gettext.dgettext(Carbon.Gettext, "errors", msg, opts) 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :carbon, 10 | ecto_repos: [Carbon.Repo] 11 | 12 | # Configures the endpoint 13 | config :carbon, Carbon.Endpoint, 14 | url: [host: "localhost"], 15 | root: Path.dirname(__DIR__), 16 | secret_key_base: "u/SYPEI6LdXu/69/HCC+y9CRZ7byzOkQcbOJwkLEPRsTRnN5Dxc3xldEz3DzkJ+K", 17 | render_errors: [view: Carbon.ErrorView, accepts: ~w(html json)], 18 | pubsub: [name: Carbon.PubSub, 19 | adapter: Phoenix.PubSub.PG2] 20 | 21 | # Configures Elixir's Logger 22 | config :logger, :console, 23 | format: "$time $metadata[$level] $message\n", 24 | metadata: [:request_id] 25 | 26 | config :number, currency: [ 27 | unit: "$", 28 | precision: 2, 29 | delimiter: "\u00A0", 30 | separator: ",", 31 | format: "%n\%u", # "30.00 $" 32 | negative_format: "-\u00A0%n\u00A0%u" # "- 30.00 $" 33 | ], 34 | delimit: [ 35 | precision: 2, 36 | delimiter: " ", 37 | separator: "," 38 | ] 39 | 40 | 41 | 42 | # Import environment specific config. This must remain at the bottom 43 | # of this file so it overrides the configuration defined above. 44 | import_config "#{Mix.env}.exs" 45 | -------------------------------------------------------------------------------- /web/templates/user/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Users

5 |
6 |
7 | <%= link to: user_path(@conn, :new), class: "ui green button " do %> 8 | 9 | Create user 10 | <% end %> 11 |
12 |
13 |
14 |
15 | <%= for user <- @users do %> 16 | <%= link to: user_path(@conn, :show, user.id), class: "ui card" do %> 17 |
18 | 19 |
20 |
21 |
22 | <%= user.full_name %> 23 |
24 |
25 | <%= user.title %> 26 |
27 |
28 |
29 | 30 | <%= user.handle %> 31 |
32 | <%= if Enum.count(user.roles) > 0 do %> 33 |
34 | 35 | <%= user.roles |> Enum.map(&(&1.key)) |> Enum.join(", ") %> 36 |
37 | <% end %> 38 | <%= if user.email do %> 39 |
40 | 41 | <%= user.email %> 42 |
43 | <% end %> 44 |
45 |
46 | <% end %> 47 | <% end %> 48 | <%= if assigns[:paginator] do %> 49 | <%= paginate(@conn, @paginator, :user) %> 50 | <% end %> 51 |
52 | -------------------------------------------------------------------------------- /web/models/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Event do 2 | use Carbon.Web, :model 3 | 4 | schema "events" do 5 | field :description, :string 6 | field :date, Ecto.Date 7 | field :private, :boolean, default: false 8 | field :active, :boolean, default: true 9 | 10 | belongs_to :user, Carbon.User 11 | belongs_to :account, Carbon.Account 12 | has_many :reminders, Carbon.Reminder 13 | many_to_many :tags, Carbon.EventTag, join_through: "j_events_tags", on_replace: :delete 14 | 15 | timestamps 16 | end 17 | 18 | @doc """ 19 | Builds a changeset based on the `struct` and `params`. 20 | """ 21 | def changeset(struct, params \\ %{}) do 22 | struct 23 | |> cast(params, [:description, :date, :private]) 24 | |> validate_required([:description, :date]) 25 | end 26 | 27 | def create_changeset(struct, params \\ %{}, tags) do 28 | struct 29 | |> cast(params, [:description, :date, :private]) 30 | |> validate_required([:description, :date]) 31 | |> put_assoc(:tags, Enum.map(tags, &Ecto.Changeset.change/1)) 32 | |> foreign_key_constraint(:user_id) 33 | |> foreign_key_constraint(:status_id) 34 | end 35 | 36 | def archive_changeset(struct, params \\ %{}) do 37 | struct 38 | |> cast(params, [:active]) 39 | |> validate_required([:active]) 40 | end 41 | 42 | def update_changeset(struct, params, tags) do 43 | struct 44 | |> cast(params, [:description, :date, :private, :user_id]) 45 | |> validate_required([:description, :date, :user_id]) 46 | |> put_assoc(:tags, Enum.map(tags, &Ecto.Changeset.change/1)) 47 | |> foreign_key_constraint(:user_id) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160813142546_create_timesheet.exs: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Repo.Migrations.CreateTimesheet do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:timesheet_statuses) do 6 | add :key, :string, null: false 7 | add :active, :boolean, default: true, null: false 8 | timestamps 9 | end 10 | create table(:timesheets) do 11 | add :lock_version, :integer, default: 1, null: false 12 | add :active, :boolean, default: true, null: false 13 | add :start_date, :date, null: false 14 | add :notes, :text 15 | add :status_id, references(:timesheet_statuses) 16 | add :user_id, references(:users) 17 | timestamps 18 | end 19 | create table(:timesheet_entries) do 20 | add :lock_version, :integer, default: 1, null: false 21 | add :active, :boolean, default: true, null: false 22 | add :duration_in_minutes, :integer, default: 0, null: false 23 | add :date, :date, null: false 24 | add :notes, :text 25 | add :billable, :boolean, default: true 26 | add :timesheet_id, references(:timesheets) 27 | add :account_id, references(:accounts) 28 | add :project_id, references(:projects) 29 | timestamps 30 | end 31 | create table(:timesheet_entry_tags) do 32 | add :description, :string 33 | add :color, :string 34 | add :active, :boolean, default: true, null: false 35 | timestamps 36 | end 37 | create table(:j_timesheet_entries_tags, primary_key: false) do 38 | add :timesheet_entry_id, references(:timesheet_entries) 39 | add :timesheet_entry_tag_id, references(:timesheet_entry_tags) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /web/views/workflow/instance_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.InstanceView do 2 | use Carbon.Web, :view 3 | import Ecto.Query, only: [from: 2] 4 | alias Carbon.{ Repo } 5 | 6 | alias Carbon.Workflow.{ Field, State } 7 | alias Carbon.{Account, User, Timesheet, Workflow} 8 | 9 | def user_select() do 10 | query = from u in User, 11 | where: u.active, 12 | select: %{id: u.id, full_name: u.full_name, image_url: u.image_url} 13 | Repo.all(query) 14 | end 15 | 16 | def timesheet_select() do 17 | query = from t in Timesheet, 18 | where: t.active, 19 | preload: [:status, :user] 20 | Repo.all(query) 21 | end 22 | 23 | def workflow_select() do 24 | # query = from w in Workflow 25 | Repo.all(Workflow) 26 | end 27 | 28 | def workflow_states_select() do 29 | # query = from w in Workflow 30 | Repo.all(State) 31 | end 32 | 33 | def account_select() do 34 | query = from a in Account, 35 | where: a.active, 36 | preload: [:status] 37 | Repo.all(query) 38 | end 39 | 40 | def account_by_id(id) do 41 | Repo.get(Account, id) 42 | end 43 | 44 | def user_by_id(id) do 45 | Repo.get(User, id) 46 | end 47 | 48 | def timesheet_by_id(id) do 49 | Repo.get(Timesheet, id) |> Repo.preload([:status, :user]) 50 | end 51 | 52 | def instance_match_params(instance, conn) do 53 | accepted_workflow_values = [nil, "", to_string(instance.workflow_id)] 54 | accepted_state_values = [nil, "", to_string(instance.state_id)] 55 | Enum.member?(accepted_workflow_values, conn.params["workflow"]) 56 | and Enum.member?(accepted_state_values, conn.params["state"]) 57 | end 58 | 59 | 60 | end 61 | -------------------------------------------------------------------------------- /lib/carbon/email_notification_sender.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.EmailNotificationSender do 2 | use GenServer 3 | 4 | import Ecto.Query, only: [from: 2] 5 | 6 | @five_minutes_in_millis 5 * 60 * 1000 7 | 8 | def start_link do 9 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 10 | end 11 | 12 | def init(:ok) do 13 | do_loop 14 | { :ok, %{} } 15 | end 16 | 17 | def handle_info(:check_and_send, state) do 18 | query = from r in Carbon.Reminder, 19 | join: u in assoc(r, :user), 20 | join: e in assoc(r, :event), 21 | left_join: et in assoc(e, :tags), 22 | where: e.active and r.active and u.active and not r.seen and not r.sent_by_email and u.send_email_reminders and ago(1, "hour") <= r.date and r.date < from_now(1, "minute"), 23 | preload: [ 24 | user: u, 25 | event: { e, tags: et }, 26 | ] 27 | 28 | for {_user_id, reminders} <- Carbon.Repo.all(query) |> Enum.group_by(&(&1.user_id)) do 29 | send_and_update(reminders) 30 | end 31 | 32 | do_loop 33 | { :noreply, state } 34 | end 35 | 36 | defp do_loop do 37 | Process.send_after(__MODULE__, :check_and_send, @five_minutes_in_millis) 38 | end 39 | 40 | defp send_and_update(reminders_for_one_user) do 41 | Carbon.Mailer.send_notification(reminders_for_one_user) 42 | |> set_all_as_sent(reminders_for_one_user) 43 | end 44 | 45 | defp set_all_as_sent(:ok, reminders) do 46 | ids_to_update = Enum.map(reminders, &(&1.id)) 47 | query = from r in Carbon.Reminder, where: r.id in ^ids_to_update 48 | Carbon.Repo.update_all query, set: [ sent_by_email: true ] 49 | end 50 | defp set_all_as_sent(:error, _reminders), do: nil 51 | 52 | end 53 | -------------------------------------------------------------------------------- /web/templates/reminder/new.html.eex: -------------------------------------------------------------------------------- 1 | 4 | 5 |

New reminder

6 | 7 | <%= form_for @changeset, account_event_reminder_path(@conn, :create, @account_id, @event_id), fn f -> %> 8 |
"> 9 |
10 |
Ruh roh!
11 |

Something went wrong. Please check the following errors.

12 |
13 | 14 |
15 |
"> 16 | 21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 |
"> 29 | 34 |
35 | 36 |
37 |
38 |
39 |
40 | 41 | 42 | <%= submit "Create reminder", class: "ui submit button" %> 43 | <%= link "cancel", to: account_event_path(@conn, :index, @account_id) %> 44 | 45 |
46 | <% end %> 47 | -------------------------------------------------------------------------------- /web/models/reminder.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Reminder do 2 | use Carbon.Web, :model 3 | 4 | schema "reminders" do 5 | field :active, :boolean, default: true 6 | 7 | field :date, Ecto.DateTime 8 | field :seen, :boolean, default: false 9 | field :sent_by_email, :boolean, default: false 10 | 11 | belongs_to :event, Carbon.Event 12 | belongs_to :user, Carbon.User 13 | 14 | timestamps 15 | end 16 | 17 | @doc """ 18 | Builds a changeset based on the `struct` and `params`. 19 | """ 20 | def changeset(struct, params \\ %{}) do 21 | struct 22 | |> cast(params, [:date]) 23 | |> validate_required([:date]) 24 | end 25 | 26 | 27 | defp merge_date_time_to_datetime(%{"date" => date, "time" => time} = params) when date != "" and time != ""do 28 | datetime = Ecto.DateTime.cast!("#{date} #{time}:00") 29 | Map.put(params, "date", datetime) 30 | end 31 | defp merge_date_time_to_datetime(params) do 32 | Map.delete(params, "date") 33 | end 34 | 35 | def create_changeset(struct, params) do 36 | struct 37 | |> cast(merge_date_time_to_datetime(params), [:date]) 38 | |> validate_required([:date]) 39 | |> copyDateErrorToTimeError() 40 | end 41 | 42 | defp copyDateErrorToTimeError(changeset) do 43 | if Map.has_key?(changeset, :errors) and Keyword.has_key?(changeset.errors, :date) do 44 | Map.update! changeset, :errors, fn current_errors -> 45 | Keyword.put current_errors, :time, {"is invalid", [type: Ecto.Time]} 46 | end 47 | else 48 | changeset 49 | end 50 | end 51 | 52 | 53 | def archive_changeset(struct, params) do 54 | struct 55 | |> cast(params, [:active]) 56 | |> validate_required([:active]) 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /web/models/timesheet.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Timesheet do 2 | use Carbon.Web, :model 3 | 4 | schema "timesheets" do 5 | field :lock_version, :integer, default: 1 6 | field :active, :boolean, default: true 7 | 8 | field :start_date, Ecto.Date 9 | field :notes, :string 10 | 11 | belongs_to :status, Carbon.TimesheetStatus 12 | belongs_to :user, Carbon.User 13 | has_many :entries, Carbon.TimesheetEntry 14 | 15 | timestamps 16 | end 17 | 18 | 19 | @doc """ 20 | Compute total of minutes for billable and non billable time. Returns a map 21 | in the following format %{billables: 21, non_billables: 42} 22 | """ 23 | def total_billables_and_non_billables(timesheet) do 24 | Enum.reduce(timesheet.entries, %{billables: 0, non_billables: 0}, &entry_totaler/2) 25 | end 26 | defp entry_totaler(%{billable: true, duration_in_minutes: duration_in_minutes}, acc) do 27 | %{acc | billables: acc.billables + duration_in_minutes} 28 | end 29 | defp entry_totaler(%{billable: false, duration_in_minutes: duration_in_minutes}, acc) do 30 | %{acc | non_billables: acc.non_billables + duration_in_minutes} 31 | end 32 | 33 | @doc """ 34 | Builds a changeset based on the `struct` and `params`. 35 | """ 36 | def update_changeset(struct, params \\ %{}) do 37 | struct 38 | |> cast(params, [:start_date, :notes, :status_id]) 39 | |> validate_required([:start_date, :status_id]) 40 | end 41 | 42 | def create_changeset(struct, params \\ %{}) do 43 | struct 44 | |> cast(params, [:start_date, :notes, :status_id]) 45 | |> validate_required([:start_date, :status_id]) 46 | end 47 | def archive_changeset(struct, params \\ %{}) do 48 | struct 49 | |> cast(params, [:active]) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /web/controllers/workflow/state_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Workflow.StateController do 2 | use Carbon.Web, :controller 3 | alias Carbon.{Workflow} 4 | alias Carbon.Workflow.{State} 5 | 6 | 7 | def new(conn, _params) do 8 | state = %State{} 9 | changeset = State.changeset(state, %{}) 10 | conn 11 | |> assign(:changeset, changeset) 12 | |> render("new.html") 13 | end 14 | 15 | def create(conn, %{"workflow_id" => workflow_id, "state" => state_params}) do 16 | workflow = Repo.get(Workflow, workflow_id) |> Repo.preload([:states]) 17 | max_index = Enum.reduce workflow.states, 0, &(max(&1.presentation_order_index, &2)) 18 | state = %State{workflow: workflow, presentation_order_index: max_index + 1} 19 | changeset = State.changeset(state, state_params) 20 | case Repo.insert(changeset) do 21 | {:ok, _state} -> 22 | conn 23 | |> put_flash(:info, "State created successfully") 24 | |> redirect(to: workflow_path(conn, :edit, workflow_id)) 25 | {:error, changeset} -> 26 | conn 27 | |> assign(:changeset, changeset) 28 | |> render("new.html") 29 | end 30 | end 31 | 32 | def delete(conn, %{"workflow_id" => workflow_id, "id" => state_id}) do 33 | state = Repo.get(State, state_id) 34 | changeset = State.changeset(state, %{active: false}) 35 | case Repo.update changeset do 36 | {:ok, _state} -> 37 | conn 38 | |> put_flash(:info, "State was successfully deleted") 39 | |> redirect(to: workflow_path(conn, :edit, workflow_id)) 40 | {:error, _changeset} -> 41 | conn 42 | |>put_flash(:info, "Failed to delete state") 43 | |> redirect(to: workflow_path(conn, :edit, workflow_id)) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /web/templates/tag/edit.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, tag_path(@conn, :update, @tag.id, tagged: @tagged), fn f -> %> 2 | 3 | <%= hidden_input f, :tagged, value: @tagged %> 4 | 5 |
"> 6 |
7 |
Ruh roh!
8 |

Something went wrong. Please check the following errors.

9 |
10 | 11 |
"> 12 | 17 |
18 | <%= text_input f, :description %> 19 |
20 |
21 |
22 | 23 |
24 | 25 | 38 |
39 | 40 | <%= submit "Update tag", class: "ui submit primary button" %> 41 | <%= link "cancel", to: tag_path(@conn, :index), class: "ui button" %> 42 |
43 | <% end %> 44 | 47 | -------------------------------------------------------------------------------- /web/models/timesheet_entry.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.TimesheetEntry do 2 | use Carbon.Web, :model 3 | 4 | schema "timesheet_entries" do 5 | field :lock_version, :integer, default: 1 6 | field :active, :boolean, default: true 7 | 8 | field :duration_in_minutes, :integer, default: 0 9 | field :date, Ecto.Date 10 | field :notes, :string 11 | field :billable, :boolean, default: true 12 | 13 | belongs_to :timesheet, Carbon.Timesheet 14 | belongs_to :project, Carbon.Project 15 | # FIXTHIS: Dénormalisation douteuse... 16 | belongs_to :account, Carbon.Account 17 | many_to_many :tags, Carbon.TimesheetEntryTag, join_through: "j_timesheet_entries_tags", on_replace: :delete 18 | 19 | timestamps 20 | end 21 | 22 | @doc """ 23 | Builds a changeset based on the `struct` and `params`. 24 | """ 25 | def create_changeset(struct, params \\ %{}, tags \\ []) do 26 | struct 27 | |> cast(params, [:date, :notes, :billable]) 28 | |> put_assoc(:tags, Enum.map(tags, &Ecto.Changeset.change/1)) 29 | |> validate_required([:duration_in_minutes, :date, :billable, :project, :account]) 30 | |> foreign_key_constraint(:project_id) 31 | |> foreign_key_constraint(:account_id) 32 | end 33 | 34 | def update_changeset(struct, params \\ %{}, tags \\ []) do 35 | struct 36 | |> cast(params, [:date, :notes, :billable, :duration_in_minutes, :project_id, :account_id]) 37 | |> put_assoc(:tags, Enum.map(tags, &Ecto.Changeset.change/1)) 38 | |> validate_required([:duration_in_minutes, :date, :billable, :project, :account]) 39 | |> foreign_key_constraint(:project_id) 40 | |> foreign_key_constraint(:account_id) 41 | end 42 | 43 | def archive_changeset(struct, params \\ %{}) do 44 | struct 45 | |> cast(params, [:active]) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /web/templates/tag/new.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, tag_path(@conn, :create, tagged: @tagged), fn f -> %> 2 | 3 | <%= hidden_input f, :tagged, value: @tagged %> 4 | 5 |
"> 6 |
7 |
Ruh roh!
8 |

Something went wrong. Please check the following errors.

9 |
10 | 11 |
"> 12 | 17 |
18 | <%= text_input f, :description %> 19 |
20 |
21 |
22 | 23 |
24 | 25 | 38 |
39 | 40 | <%= submit "Create tag", class: "ui submit primary button" %> 41 | <%= link "cancel", to: tag_path(@conn, :index), class: "ui button" %> 42 |
43 | <% end %> 44 | 47 | -------------------------------------------------------------------------------- /web/templates/tag/_show_card.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%= String.capitalize @tagged %> tags

3 | 4 |
5 | <%= for tag <- @tags do %> 6 |
7 |
8 | 9 |
<%= tag.description %>
10 | <% 11 | message = case Map.get(@occurrences, tag.id, 0) do 12 | 0 -> "no #{@tagged}s tagged yet!" 13 | 1 -> "1 #{@tagged} tagged" 14 | n -> "#{n} #{@tagged}s tagged" 15 | end 16 | %> 17 |
<%= message %>
18 |
19 |
20 | <%= link to: tag_path(@conn, :edit, tag.id, tagged: @tagged) do %> 21 | Edit 22 | <% end %> 23 | <%= link method: "delete", to: tag_path(@conn, :delete, tag.id, tagged: @tagged) do %> 24 | Remove 25 | <% end %> 26 |
27 |
28 | <% end %> 29 | <%= link to: tag_path(@conn, :new, tagged: @tagged), class: "ui card" do %> 30 |
31 | 32 |
create new…
33 |
no <%= @tagged %>s tagged yet!
34 |
35 |
36 |
Create…
37 |
38 | <% end %> 39 |
40 |
41 | -------------------------------------------------------------------------------- /web/views/account_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.AccountView do 2 | use Carbon.Web, :view 3 | import Carbon.ViewHelpers, only: [account_status_select: 0, account_tags_select: 0, account_user_select: 0, humanize: 2, probability_color: 1] 4 | import Carbon.PaginatorHelper 5 | 6 | def match_table_to_color("account"), do: "blue" 7 | def match_table_to_color("event"), do: "yellow" 8 | def match_table_to_color("contact"), do: "purple" 9 | def match_table_to_color("project"), do: "pink" 10 | def match_table_to_color(_model), do: "grey" 11 | 12 | def events_summary(all_events, reference_date \\ :calendar.local_time |> elem(0) |> Ecto.Date.from_erl) do 13 | prev_events = all_events 14 | |> Enum.filter(<_date(&1.date, reference_date)) 15 | |> Enum.sort_by(&(&1.date), <e_date/2) 16 | |> slice_right(5) 17 | |> Enum.reverse 18 | next_events = all_events 19 | |> Enum.filter(>e_date(&1.date, reference_date)) 20 | |> Enum.sort_by(&(&1.date), <e_date/2) 21 | |> slice_left(5) 22 | |> Enum.reverse 23 | 24 | { prev_events, next_events } 25 | end 26 | 27 | defp slice_left(list, len) when length(list) > len, do: Enum.slice(list, 0..len-1) 28 | defp slice_left(list, _len), do: list 29 | 30 | defp slice_right(list, len) when length(list) > len, do: Enum.slice(list, -len..-1) 31 | defp slice_right(list, _len), do: list 32 | 33 | defp lt_date(d1, d2) do 34 | case Ecto.Date.compare(d1, d2) do 35 | :lt -> true 36 | _ -> false 37 | end 38 | end 39 | defp gte_date(d1, d2) do 40 | case Ecto.Date.compare(d1, d2) do 41 | :lt -> false 42 | _ -> true 43 | end 44 | end 45 | defp lte_date(d1, d2) do 46 | case Ecto.Date.compare(d1, d2) do 47 | :gt -> false 48 | _ -> true 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /web/templates/activity/index.html.eex: -------------------------------------------------------------------------------- 1 | 9 | 10 |

11 |
12 | Activity 13 |

14 | <%= @account.name %> 15 |

16 |
17 |

18 |
19 | <%= for activity <- @activities do %> 20 |
21 |
22 | 23 |
24 |
25 |
26 | <%= activity.user.full_name %> 27 | <%= past_tense activity.action %> <%= singular activity.entity_name %> #<%= activity.entity_id %> 28 |
29 | <%= activity.inserted_at %> GMT 30 |
31 |
32 |
33 | <%= if activity.changes != "" do %> 34 | <%= case activity.action do %> 35 | <%= "create" -> %> 36 | <%= "remove" -> %> 37 | <%= "update" -> %> 38 | <%= "restore" -> %> 39 | <% end %> 40 | <% field_names = activity.changes |> String.split(",") |> Enum.map(&for_humans/1) %> 41 | <%= for { name, reverse_index } <- Enum.zip(field_names, (length field_names)-1..0) do %> 42 | <%= name %><%= if reverse_index > 1, do: ", " %><%= if reverse_index == 1, do: " and " %><%= if reverse_index == 0, do: "." %> 43 | <% end %> 44 | <% end %> 45 |
46 |
47 |
48 | <% end %> 49 |
50 | -------------------------------------------------------------------------------- /web/views/paginator_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.PaginatorHelper do 2 | use Phoenix.HTML 3 | alias Carbon.Paginator 4 | 5 | def paginate(conn, paginator = %Paginator{}, module) do 6 | if paginator.number_of_pages > 1 do 7 | content_tag :div, class: "ui center aligned container" do 8 | [ 9 | get_previous_link(conn, paginator, module), 10 | get_links(conn, paginator, module), 11 | get_next_link(conn, paginator, module) 12 | ] 13 | end 14 | end 15 | end 16 | 17 | defp get_previous_link(conn, paginator = %Paginator{}, module) do 18 | css_class = if paginator.page == 1 do "disabled" else "basic" end 19 | 20 | link content_tag(:i, "", class: "ui icon angle left"), 21 | to: generate_url(conn, module, [page: paginator.page-1, length: paginator.length]), 22 | class: "ui button " <> css_class 23 | end 24 | 25 | defp get_next_link(conn, paginator = %Paginator{}, module) do 26 | css_class = if paginator.page == paginator.number_of_pages do "disabled" else "basic" end 27 | 28 | link content_tag(:i, "", class: "ui icon angle right"), 29 | to: generate_url(conn, module, [page: paginator.page+1, length: paginator.length]), 30 | class: "ui button " <> css_class 31 | end 32 | 33 | defp get_links(conn, paginator = %Paginator{}, module) do 34 | links = for page_number <- 1..paginator.number_of_pages do 35 | url_path = generate_url(conn, module, [page: page_number, length: paginator.length]) 36 | 37 | css_class = if page_number == paginator.page do " active" else "" end 38 | 39 | link page_number, 40 | to: url_path, 41 | class: "ui button basic" <> css_class 42 | end 43 | links 44 | end 45 | 46 | defp generate_url(conn, module, args) do 47 | Kernel.apply Carbon.Router.Helpers, 48 | String.to_atom("#{module}_path"), 49 | [conn, String.to_atom("#{conn.private.phoenix_action}"), args] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use Carbon.Web, :controller 9 | use Carbon.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def model do 20 | quote do 21 | use Ecto.Schema 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | end 27 | end 28 | 29 | def controller do 30 | quote do 31 | use Phoenix.Controller 32 | 33 | alias Carbon.Repo 34 | import Ecto 35 | import Ecto.Query 36 | 37 | import Carbon.Router.Helpers 38 | import Carbon.Gettext 39 | end 40 | end 41 | 42 | def view do 43 | quote do 44 | use Phoenix.View, root: "web/templates" 45 | 46 | # Import convenience functions from controllers 47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 48 | 49 | # Use all HTML functionality (forms, tags, etc) 50 | use Phoenix.HTML 51 | 52 | import Carbon.Router.Helpers 53 | import Carbon.ErrorHelpers 54 | import Carbon.Gettext 55 | end 56 | end 57 | 58 | def router do 59 | quote do 60 | use Phoenix.Router 61 | end 62 | end 63 | 64 | def channel do 65 | quote do 66 | use Phoenix.Channel 67 | 68 | alias Carbon.Repo 69 | import Ecto 70 | import Ecto.Query 71 | import Carbon.Gettext 72 | end 73 | end 74 | 75 | @doc """ 76 | When used, dispatch to the appropriate controller/view/etc. 77 | """ 78 | defmacro __using__(which) when is_atom(which) do 79 | apply(__MODULE__, which, []) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /web/templates/attachment/index.html.eex: -------------------------------------------------------------------------------- 1 | 4 | 5 |

6 |
Attachments 7 |

<%= @account.name %>

8 |
9 | <%= link to: account_attachment_path(@conn, :new, @account.id) do %> 10 |
Add attachment
11 | <% end %> 12 |

13 | 14 |
15 | <%= for attachment <- @attachments do %> 16 |
17 |
18 | <%= link to: account_attachment_path(@conn, :show, @account.id, attachment.id), data: [turbolinks: false] do %> 19 | 20 | <% end %> 21 |
22 |
23 |
24 | <%= link to: account_attachment_path(@conn, :show, @account.id, attachment.id), data: [turbolinks: false] do %><%= attachment.name %><% end %> 25 |
26 | <%= attachment.inserted_at |> Ecto.DateTime.to_date %> 27 | <%= if attachment.private do %><% end %> 28 |
29 |
30 |
<%= attachment.description %>
31 |
32 | <%= link to: account_attachment_path(@conn, :show, @account.id, attachment.id), data: [turbolinks: false] do %> Download<% end %> 33 | <%= link to: account_attachment_path(@conn, :edit, @account.id, attachment.id) do %> Edit<% end %> 34 | <%= link method: "delete", to: account_attachment_path(@conn, :delete, @account.id, attachment.id) do %> Remove<% end %> 35 |
36 |
37 |
38 | <% end %> 39 |
40 | -------------------------------------------------------------------------------- /web/templates/attachment/as_card_list.html.eex: -------------------------------------------------------------------------------- 1 | <%= for attachment <- @account.attachments do %> 2 |
3 |
4 |
5 |
<%= link attachment.name, to: account_attachment_path(@conn, :show, @account.id, attachment.id), data: [turbolinks: "false"] %>
6 |
<%= attachment.inserted_at |> Ecto.DateTime.to_date %>
7 |

<%= attachment.description %>

8 |
9 |
10 | <%= if attachment.private do %> 11 |
12 | 13 |
14 | <% end %> 15 |
16 | <%= attachment.user.handle %> 17 |
18 |
19 |
20 | <% end %> 21 | <%= link to: account_attachment_path(@conn, :new, @account.id), class: "ui card" do %> 22 |
23 |
24 |
add new…
25 |
<%= :calendar.local_time |> elem(0) |> Date.from_erl! |> Date.to_iso8601 %>
26 |

Attach a new private or public file.

27 |
28 |
29 |
Add…
30 |
31 | <% end %> 32 | 33 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Carbon.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias Carbon.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import Carbon.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Carbon.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(Carbon.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | Helper for returning list of errors in a struct when given certain data. 40 | 41 | ## Examples 42 | 43 | Given a User schema that lists `:name` as a required field and validates 44 | `:password` to be safe, it would return: 45 | 46 | iex> errors_on(%User{}, %{password: "password"}) 47 | [password: "is unsafe", name: "is blank"] 48 | 49 | You could then write your assertion like: 50 | 51 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 52 | 53 | You can also create the changeset manually and retrieve the errors 54 | field directly: 55 | 56 | iex> changeset = User.changeset(%User{}, password: "password") 57 | iex> {:password, "is unsafe"} in changeset.errors 58 | true 59 | """ 60 | def errors_on(struct, data) do 61 | struct.__struct__.changeset(struct, data) 62 | |> Ecto.Changeset.traverse_errors(&Carbon.ErrorHelpers.translate_error/1) 63 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) 64 | end 65 | end 66 | --------------------------------------------------------------------------------