diff --git a/.gitignore b/.gitignore
index 06e011c54..826fb9cf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
*.DS_Store
.DS_Store
media/
+staticfiles
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..8cc3428a9
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,82 @@
+# Code of Conduct
+
+## 1. Purpose
+
+A primary goal of CiviWiki is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
+
+This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
+
+We invite all those who participate in CiviWiki to help us create safe and positive experiences for everyone.
+
+## 2. Open Source Citizenship
+
+A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
+
+Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
+
+If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
+
+## 3. Expected Behavior
+
+The following behaviors are expected and requested of all community members:
+
+* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
+* Exercise consideration and respect in your speech and actions.
+* Attempt collaboration before conflict.
+* Refrain from demeaning, discriminatory, or harassing behavior and speech.
+* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
+* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
+
+## 4. Unacceptable Behavior
+
+The following behaviors are considered harassment and are unacceptable within our community:
+
+* Violence, threats of violence or violent language directed against another person.
+* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
+* Posting or displaying sexually explicit or violent material.
+* Posting or threatening to post other people’s personally identifying information ("doxing").
+* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
+* Inappropriate photography or recording.
+* Inappropriate physical contact. You should have someone’s consent before touching them.
+* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
+* Deliberate intimidation, stalking or following (online or in person).
+* Advocating for, or encouraging, any of the above behavior.
+* Sustained disruption of community events, including talks and presentations.
+
+## 5. Consequences of Unacceptable Behavior
+
+Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
+
+Anyone asked to stop unacceptable behavior is expected to comply immediately.
+
+If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
+
+## 6. Reporting Guidelines
+
+If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. mitchell.west@civiwiki.org.
+
+Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
+
+## 7. Addressing Grievances
+
+If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify CiviWiki with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
+
+Link to policy: Mitchell
+
+## 8. Scope
+
+We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business.
+
+This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
+
+## 9. Contact info
+
+mitchell.west@civiwiki.org
+
+## 10. License and attribution
+
+This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
+
+Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
+
+Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b5903b7d4..0fe687849 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
Use these instructions to set up a local development environment.
## Prerequisites
-The following packages are helpful/required to start developing CiviWiki:
+The following packages are required to start developing CiviWiki:
- PostgreSQL
- Redis
@@ -72,14 +72,14 @@ python manage.py migrate
If everything goes well, you should see a bunch of green 'OK's. Django will create the database structure for you.
### Populate initial data
-There are two files, located in the `/data` directory, that contain initial categories and congressional data. Run the following commands to initialize the congressional and categoies data:
+There are two files, located in the `/data` directory, that contain initial categories and congressional data. Run the following commands to initialize the congressional and categories data:
```py
-python manage.py loaddata /data/congress.json
+python manage.py loaddata data/congress.json
```
```py
-python manage.py loaddata /data/categories.json
+python manage.py loaddata data/categories.json
```
### Collect static files
@@ -106,5 +106,21 @@ python manage.py runserver
## Register initial user
Once CiviWiki is running, visit the front page (probably something like http://localhost:8000). Once there, click 'log in/register', and then 'register new user'.
-### Enable beta access for new user
-Once you have submitted the new user registration fom, you will be directed to the 'Development in progress' screen. In order to access the site functionality, you need to modify the new user record and set the `beta_access` field to `True` for the user record in the `api_account` table.
+# Coding Conventions
+
+We strive to follow Django Coding Conventions. See https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
+
+# Compatible Versioning
+We use Compatibile Versioning in this project.
+
+Given a version number MAJOR.MINOR, increment the:
+
+MAJOR version when you make backwards-incompatible updates of any kind
+MINOR version when you make 100% backwards-compatible updates
+Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR format.
+
+## How is this different to SemVer?
+
+Compatible Versioning ("ComVer") is SemVer where every PATCH number is 0 (zero). This way, ComVer is backwards compatible with SemVer.
+
+A ComVer release from 3.6 to 3.7 is just a SemVer release from 3.6.0 to 3.7.0. In other words, ComVer is safe to adopt since it is basically SemVer without ever issuing PATCH releases.
diff --git a/Procfile b/Procfile
index 4ec1e36c7..4489ad072 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: honcho -f Procfile.prod start --port $PORT
+web: honcho -f project/Procfile.prod start --port $PORT
diff --git a/Procfile.prod b/Procfile.prod
deleted file mode 100644
index 5cb958015..000000000
--- a/Procfile.prod
+++ /dev/null
@@ -1,3 +0,0 @@
-web: daphne civiwiki.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2
-worker: python manage.py runworker -v2
-celery: celery -A civiwiki worker --app=civiwiki.celery:app -l info
diff --git a/README.md b/README.md
index b8b47d76e..0fbaff02b 100644
--- a/README.md
+++ b/README.md
@@ -1,89 +1,21 @@
+[![ComVer](https://img.shields.io/badge/ComVer-compliant-brightgreen.svg)](https://github.com/staltz/comver)
[![Stories in Ready](https://badge.waffle.io/CiviWiki/OpenCiviWiki.png?label=ready&title=Ready)](https://waffle.io/CiviWiki/OpenCiviWiki?utm_source=badge)
-Welcome to Civiwiki!
--------------------
+
+# Welcome to Civiwiki!
We are an open source, non-profit community, working to develop a democratic engagement web system.
-Why CiviWiki?
+## Why CiviWiki?
-* **Democratically Contributed Media.** As the name CiviWiki implies, our core content will be contributed by volunteers on our Wiki. Our topic format is modular. The structure both allows a community of volunteers to collaborate on a single political issue, and reserves space for dissenting opinions.
+* **Democratically Contributed Media.** As the name CiviWiki implies, our core content will be contributed by volunteers on our Wiki. Our topic format is modular. The structure allows both a community of volunteers to collaborate on a single political issue, and reserves space for dissenting opinions.
* **Personalized Policy Feed.** CiviWiki intelligently personalizes users' feed in two meaningful ways. First, the issues promoted to users' feed will be personalized to the user's expressed interests, and the timeliness of the issue. Second, the structure of the issue topics break policy positions into bite-sized contentions we call Civies. Each Civi is logically related to the rest of the topic. Based on the user's support, opposition, or neutrality to each Civi, CiviWiki promotes different relevant content.
-* **Citizen/Representative Engagement.** CiviWiki's core goal is to engage citizens and their representatives, with the goal of making government more accountable. CiviWiki will achieves this goal in two ways. First, CiviWiki will organize user's policy profile and compare it to every political candidate in the user's district. This quick, detailed, comparison will help users make informed votes, and we believe increased voter confidence will increase voter turnout. Second, CiviWiki will collect anonymized user data and forward district level statistics to representatives. With a critical mass of users, we believe timely district level polling data will influence representatives' votes.
-
-For Developers.
----------------
-
-**NOTE: THIS README IS OUTDATED AND NEEDS TO BE UPDATED ACCORDINGLY**
-
-**Setup**: Setup requires certain environment variables on your machine to be set in order for the application to access secret values, these values are listed below.
-* Clone or Fork our repository.
-* Create a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/).
-* `pip install -r requirements.txt`.
-* Ensure you have a [database](https://www.postgresql.org/docs/9.1/static/app-createdb.html), and a [user / password](https://www.postgresql.org/docs/9.1/static/app-createuser.html) with [all privileges](https://www.postgresql.org/docs/9.0/static/sql-grant.html), the app will look for this connection using credentials stored in enviornment variables, you can get more details on this below.
-* It is required when running `python manage.py ` that you explicitly state what settings module you are using.
- * Use `--settings=civiwiki.settings.local` to run the application with your local database credentials.
- * Use `--settings=civiwiki.settings.dev` to run the application on a development server you have hosted.
- * Use `--settings=civiwiki.settings.production` to run the application on a production server. **WARNING: Debug is False**.
-
-Versioning System
- Semantic Versioning Summary
-Given a version number MAJOR.MINOR.PATCH, increment the:
-
-MAJOR version when you make incompatible API changes,
-MINOR version when you add functionality in a backwards-compatible manner, and
-PATCH version when you make backwards-compatible bug fixes.
-Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
-
- Semantic Versioning Specifications
-The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
-
-Software using Semantic Versioning MUST declare a public API. This API could be declared in the code itself or exist strictly in documentation. However it is done, it should be precise and comprehensive.
-
-A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor version, and Z is the patch version. Each element MUST increase numerically. For instance: 1.9.0 -> 1.10.0 -> 1.11.0.
-
-Once a versioned package has been released, the contents of that version MUST NOT be modified. Any modifications MUST be released as a new version.
-
-Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable.
-
-Version 1.0.0 defines the public API. The way in which the version number is incremented after this release is dependent on this public API and how it changes.
-
-Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior.
+* **Citizen/Representative Engagement.** CiviWiki's core goal is to engage citizens and their representatives, with the goal of making government more accountable. CiviWiki will achieve this goal in two ways. First, CiviWiki will organize user's policy profile and compare it to every political candidate in the user's district. This quick, detailed, comparison will help users make informed votes, and we believe increased voter confidence will increase voter turnout. Second, CiviWiki will collect anonymized user data and forward district level statistics to representatives. With a critical mass of users, we believe timely district level polling data will influence representatives' votes.
-Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced to the public API. It MUST be incremented if any public API functionality is marked as deprecated. It MAY be incremented if substantial new functionality or improvements are introduced within the private code. It MAY include patch level changes. Patch version MUST be reset to 0 when minor version is incremented.
-
-Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced to the public API. It MAY include minor and patch level changes. Patch and minor version MUST be reset to 0 when major version is incremented.
-
-A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
-
-Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata SHOULD be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.
-
-Precedence refers to how versions are compared to each other when ordered. Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers in that order (Build metadata does not figure into precedence). Precedence is determined by the first difference when comparing each of these identifiers from left to right as follows: Major, minor, and patch versions are always compared numerically. Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. Example: 1.0.0-alpha < 1.0.0. Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by comparing each dot separated identifier from left to right until a difference is found as follows: identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens are compared lexically in ASCII sort order. Numeric identifiers always have lower precedence than non-numeric identifiers. A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
-
-Environment Variables
-
-**Below is a list of environment variables expected to be present when you run your server.** The application does not manage how you keep track of these variables ( a recommended [solution](http://stackoverflow.com/a/11134336) ) and only checks for variables that are needed at the time. For example, production database environment variables do not need to be present if developing locally.
-* **DJANGO_SECRET_KEY**: _This value **must** be in the list of environment variables._ Information on the Django Secret Key can be found [here](https://docs.djangoproject.com/en/1.8/ref/settings/#secret-key), information on generating a key can be found in this StackOverflow [post](http://stackoverflow.com/questions/4664724/distributing-django-projects-with-unique-secret-keys/16630719#16630719).
-* **SUNLIGHT_API_KEY**: _This value **must** be in the list of environment variables._ Information on retrieving a [Sunlight API](https://sunlightfoundation.com/api) Key can be found [here](https://sunlightfoundation.com/api/accounts/register/).
-* **CIVIWIKI_LOCAL_NAME:** name of database to be used when searching your localhost databases.
-* **CIVIWIKI_LOCAL_USERNAME:** username the application should use to access your localhost database.
-* **CIVWIKI_LOCAL_PASSWORD:** password the user needs to log into your localhost database.
-* **CIVIWIKI_DEV_HOST:** address where development database is hosted.
-* **CIVIWIKI_DEV_PORT:** port number to access database to _(5432 if unsure)_.
-* **CIVIWIKI_DEV_NAME:** name of database to be used when accessing databases on your server.
-* **CIVIWIKI_DEV_ENGINE:** set to _django.db.backends.postgresql_psycopg2_.
-* **CIVIWKIKI_DEV_USERNAME:** username that application should use to access your localhost database.
-* **CIVIWIKI_DEV_PASSWORD:** password the user needs to log into your localhost database.
-
-_Production settings are configured to be run on an Amazon AWS Instance connecting to their RDS services._
-
-
-
-**Contribute**:
-Contact us on Twitter to join the team.
+# Contact info
+Contact us on Twitter to join the team:
+* **Twitter:** [@CiviWiki](https://twitter.com/civiwiki)
-I want to keep track of how Civiwiki is doing.
-----------------------------------------------
-#### Contact info
+# Contribute
+See our [Contributing Guide](CONTRIBUTING.md) for instructions on how to contribute code.
-* **Twitter:** [@CiviWiki](https://twitter.com/civiwiki)
diff --git a/api/admin.py b/api/admin.py
deleted file mode 100644
index 8c38f3f3d..000000000
--- a/api/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/api/tests.py b/api/tests.py
deleted file mode 100644
index 7ce503c2d..000000000
--- a/api/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/docs/Organization/CiviWiki Open Canvas.odp b/docs/Organization/CiviWiki Open Canvas.odp
new file mode 100644
index 000000000..9a6911e41
Binary files /dev/null and b/docs/Organization/CiviWiki Open Canvas.odp differ
diff --git a/docs/design/CiviWiki_Key_Functionality_Report.odt b/docs/design/CiviWiki_Key_Functionality_Report.odt
new file mode 100644
index 000000000..315faece4
Binary files /dev/null and b/docs/design/CiviWiki_Key_Functionality_Report.odt differ
diff --git a/docs/design/LICENSE.md b/docs/design/LICENSE.md
new file mode 100644
index 000000000..a3accf840
--- /dev/null
+++ b/docs/design/LICENSE.md
@@ -0,0 +1,41 @@
+# CC0 1.0 Universal
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
+
+## Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. **Copyright and Related Rights.** A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or performer(s);
+
+ iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
+
+ iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
+
+ v. rights protecting the extraction, dissemination, use and reuse of data in a Work;
+
+ vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
+
+ vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
+
+2. **Waiver.** To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. **Public License Fallback.** Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
+
+4. **Limitations and Disclaimers.**
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
+
+ b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
+
+ c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
+
+ d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
diff --git a/docs/design/bill_api.json b/docs/design/bill_api.json
new file mode 100644
index 000000000..3c88c0156
--- /dev/null
+++ b/docs/design/bill_api.json
@@ -0,0 +1,286 @@
+{
+ "tags": [
+ {
+ "name": "Bills",
+ "description": "Operations related to searching for bills, both state and federal."
+ }
+ ],
+ "paths": {
+ "/bills/search": {
+ "get": {
+ "operationId": "bills_search",
+ "summary": "Get details for zero or more bills.",
+ "description": "This endpoint allows you to search for bills. It will return zero or more bills that match the search criteria.",
+ "tags": [
+ "Bills"
+ ],
+ "responses": {
+ "200": {
+ "description": "A JSON array containing one or more bills.",
+ "schema": {
+ "x-oad-type": "array",
+ "type": "array",
+ "title": "Results",
+ "description": "The array of bill results.",
+ "items": {
+ "x-oad-type": "reference",
+ "$ref": "#/definitions/bill"
+ }
+ },
+ "x-oad-type": "response"
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/parameters/query",
+ "x-oad-type": "reference"
+ },
+ {
+ "$ref": "#/parameters/scope",
+ "x-oad-type": "reference"
+ },
+ {
+ "$ref": "#/parameters/source",
+ "x-oad-type": "reference"
+ },
+ {
+ "$ref": "#/parameters/name",
+ "x-oad-type": "reference"
+ },
+ {
+ "$ref": "#/parameters/bill_id",
+ "x-oad-type": "reference"
+ },
+ {
+ "$ref": "#/parameters/state",
+ "x-oad-type": "reference"
+ }
+ ]
+ },
+ "x-oad-type": "operation"
+ },
+ "/bills/{pk}": {
+ "get": {
+ "operationId": "get_bill",
+ "summary": "Git a single Bill object by primary key.",
+ "tags": [
+ "Bills"
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully retrieved bill.",
+ "schema": {
+ "x-oad-type": "reference",
+ "$ref": "#/definitions/bill"
+ },
+ "x-oad-type": "response"
+ }
+ },
+ "parameters": [
+ {
+ "name": "pk",
+ "in": "path",
+ "description": "The internal (CiviWiki) primary key for the Bill.",
+ "required": true,
+ "type": "integer",
+ "format": "int32",
+ "x-oad-type": "parameter"
+ }
+ ]
+ },
+ "x-oad-type": "operation"
+ }
+ },
+ "parameters": {
+ "name": {
+ "name": "Name",
+ "in": "header",
+ "description": "The name of a bill.",
+ "required": false,
+ "type": "string",
+ "x-oad-type": "parameter"
+ },
+ "query": {
+ "name": "Query",
+ "in": "header",
+ "description": "The search query.",
+ "required": true,
+ "type": "string",
+ "x-oad-type": "parameter"
+ },
+ "scope": {
+ "name": "Scope",
+ "in": "header",
+ "description": "The scope of this bill Can be either\n\n- state\n- federal",
+ "required": true,
+ "type": "string",
+ "enum": [
+ "federal",
+ "state"
+ ],
+ "x-oad-type": "parameter"
+ },
+ "source": {
+ "name": "Source",
+ "in": "header",
+ "description": "Which source to search for bills.\n\nCan be one of:\n\n- open_states\n- propublica\n- civiwiki",
+ "required": false,
+ "type": "string",
+ "enum": [
+ "civiwiki",
+ "open_states",
+ "propublica"
+ ],
+ "x-oad-type": "parameter"
+ },
+ "bill_id": {
+ "name": "Bill ID",
+ "in": "header",
+ "description": "The official id of the bill (e.g. ‘SB 27’, ‘A 2111’)",
+ "required": false,
+ "type": "string",
+ "x-oad-type": "parameter"
+ },
+ "state": {
+ "name": "State",
+ "in": "header",
+ "description": "Two-letter USPS abbreviation for U.S. state (or district). For use in e.g. state legislation search.\n\nhttps://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations",
+ "required": false,
+ "type": "string",
+ "enum": [
+ "AL",
+ "AK",
+ "AZ",
+ "AR",
+ "CA",
+ "CO",
+ "CT",
+ "DE",
+ "DC",
+ "FL",
+ "GA",
+ "HI",
+ "ID",
+ "IL",
+ "IN",
+ "IA",
+ "KS",
+ "KY",
+ "LA",
+ "ME",
+ "MD",
+ "MA",
+ "MI",
+ "MN",
+ "MS",
+ "MO",
+ "MT",
+ "NE",
+ "NV",
+ "NH",
+ "NJ",
+ "NM",
+ "NY",
+ "NC",
+ "ND",
+ "OH",
+ "OK",
+ "OR",
+ "PA",
+ "RI",
+ "SC",
+ "SD",
+ "TN",
+ "TX",
+ " \tUT",
+ "VT",
+ "VA",
+ "WA",
+ "WV",
+ "WI",
+ "WY"
+ ],
+ "x-oad-type": "parameter"
+ }
+ },
+ "definitions": {
+ "bill": {
+ "x-oad-type": "object",
+ "type": "object",
+ "title": "Bill",
+ "description": "A single bill object",
+ "properties": {
+ "name": {
+ "x-oad-type": "string",
+ "type": "string",
+ "title": "Name",
+ "description": "The name of the bill."
+ },
+ "description": {
+ "x-oad-type": "string",
+ "type": "string",
+ "title": "Description",
+ "description": "Bill description"
+ },
+ "source_id": {
+ "x-oad-type": "string",
+ "type": "string",
+ "title": "Source ID",
+ "description": "Unique ID from Bill source (Pro Publica or Open States)"
+ },
+ "source": {
+ "x-oad-type": "string",
+ "type": "string",
+ "title": "Source",
+ "description": "Original source for information about this bill. Can be either of:\n\n- open_states\n- propublica",
+ "enum": [
+ "open_states",
+ "propublica"
+ ]
+ },
+ "bill_id": {
+ "x-oad-type": "string",
+ "type": "string",
+ "title": "Bill ID",
+ "description": "The official id of the bill (e.g. ‘SB 27’, ‘A 2111’)"
+ }
+ },
+ "required": [
+ "source",
+ "source_id"
+ ]
+ }
+ },
+ "info": {
+ "title": "CiviWiki Bill API",
+ "version": "0.1.0",
+ "description": "Read-only Bill API for the CiviWiki frontend application.",
+ "contact": {
+ "name": "Mitchell West",
+ "email": "mitchell.west@civiwiki.org",
+ "url": "https://www.civiwiki.org"
+ },
+ "license": {
+ "name": "Creative Commons Zero",
+ "url": "https://creativecommons.org/publicdomain/zero/1.0/"
+ }
+ },
+ "externalDocs": {
+ "description": "You can reach our development team on GitHub.",
+ "url": "https://github.com/CiviWiki/OpenCiviWiki"
+ },
+ "host": "civiwiki.org",
+ "basePath": "/api/v1",
+ "schemes": [
+ "https",
+ "wss"
+ ],
+ "consumes": [
+ "application/json",
+ "application/x-www-form-urlencoded"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "swagger": "2.0"
+}
\ No newline at end of file
diff --git a/legislation/admin.py b/legislation/admin.py
deleted file mode 100644
index 8c38f3f3d..000000000
--- a/legislation/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/legislation/models.py b/legislation/models.py
deleted file mode 100644
index 71a836239..000000000
--- a/legislation/models.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
diff --git a/legislation/tests.py b/legislation/tests.py
deleted file mode 100644
index 7ce503c2d..000000000
--- a/legislation/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/legislation/views.py b/legislation/views.py
deleted file mode 100644
index 91ea44a21..000000000
--- a/legislation/views.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.shortcuts import render
-
-# Create your views here.
diff --git a/Procfile.dev b/project/Procfile.dev
similarity index 100%
rename from Procfile.dev
rename to project/Procfile.dev
diff --git a/project/Procfile.prod b/project/Procfile.prod
new file mode 100644
index 000000000..dbb833445
--- /dev/null
+++ b/project/Procfile.prod
@@ -0,0 +1,3 @@
+web: sh -c 'cd ./project/ && daphne civiwiki.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2'
+worker: python project/manage.py runworker -v2
+celery: sh -c 'cd ./project/ && celery -A civiwiki worker --app=civiwiki.celery:app -l info'
diff --git a/api/__init__.py b/project/api/__init__.py
similarity index 100%
rename from api/__init__.py
rename to project/api/__init__.py
diff --git a/api/consumers.py b/project/api/consumers.py
similarity index 96%
rename from api/consumers.py
rename to project/api/consumers.py
index 62892d3f6..9dbb8e489 100644
--- a/api/consumers.py
+++ b/project/api/consumers.py
@@ -1,7 +1,6 @@
-from channels import Channel, Group
+from channels import Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http
-from api.models import Account
#
#TODO
# 1. A User follows you
diff --git a/api/forms.py b/project/api/forms.py
similarity index 100%
rename from api/forms.py
rename to project/api/forms.py
diff --git a/api/migrations/0001_initial.py b/project/api/migrations/0001_initial.py
similarity index 100%
rename from api/migrations/0001_initial.py
rename to project/api/migrations/0001_initial.py
diff --git a/api/migrations/0002_auto_20161215_0139.py b/project/api/migrations/0002_auto_20161215_0139.py
similarity index 100%
rename from api/migrations/0002_auto_20161215_0139.py
rename to project/api/migrations/0002_auto_20161215_0139.py
diff --git a/api/migrations/0003_auto_20161227_1342.py b/project/api/migrations/0003_auto_20161227_1342.py
similarity index 100%
rename from api/migrations/0003_auto_20161227_1342.py
rename to project/api/migrations/0003_auto_20161227_1342.py
diff --git a/api/migrations/0004_auto_20161230_0412.py b/project/api/migrations/0004_auto_20161230_0412.py
similarity index 100%
rename from api/migrations/0004_auto_20161230_0412.py
rename to project/api/migrations/0004_auto_20161230_0412.py
diff --git a/api/migrations/0005_auto_20170109_1813.py b/project/api/migrations/0005_auto_20170109_1813.py
similarity index 100%
rename from api/migrations/0005_auto_20170109_1813.py
rename to project/api/migrations/0005_auto_20170109_1813.py
diff --git a/api/migrations/0006_auto_20170110_0519.py b/project/api/migrations/0006_auto_20170110_0519.py
similarity index 100%
rename from api/migrations/0006_auto_20170110_0519.py
rename to project/api/migrations/0006_auto_20170110_0519.py
diff --git a/api/migrations/0007_auto_20170110_0850.py b/project/api/migrations/0007_auto_20170110_0850.py
similarity index 100%
rename from api/migrations/0007_auto_20170110_0850.py
rename to project/api/migrations/0007_auto_20170110_0850.py
diff --git a/api/migrations/0008_auto_20170110_0850.py b/project/api/migrations/0008_auto_20170110_0850.py
similarity index 100%
rename from api/migrations/0008_auto_20170110_0850.py
rename to project/api/migrations/0008_auto_20170110_0850.py
diff --git a/api/migrations/0009_auto_20170110_1828.py b/project/api/migrations/0009_auto_20170110_1828.py
similarity index 100%
rename from api/migrations/0009_auto_20170110_1828.py
rename to project/api/migrations/0009_auto_20170110_1828.py
diff --git a/api/migrations/0010_auto_20170110_1830.py b/project/api/migrations/0010_auto_20170110_1830.py
similarity index 95%
rename from api/migrations/0010_auto_20170110_1830.py
rename to project/api/migrations/0010_auto_20170110_1830.py
index 5e331147b..6ea50d22a 100644
--- a/api/migrations/0010_auto_20170110_1830.py
+++ b/project/api/migrations/0010_auto_20170110_1830.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import migrations, models
+from django.db import migrations
class Migration(migrations.Migration):
diff --git a/api/migrations/0011_activity.py b/project/api/migrations/0011_activity.py
similarity index 100%
rename from api/migrations/0011_activity.py
rename to project/api/migrations/0011_activity.py
diff --git a/api/migrations/0012_auto_20170116_0856.py b/project/api/migrations/0012_auto_20170116_0856.py
similarity index 100%
rename from api/migrations/0012_auto_20170116_0856.py
rename to project/api/migrations/0012_auto_20170116_0856.py
diff --git a/api/migrations/0013_auto_20170116_1304.py b/project/api/migrations/0013_auto_20170116_1304.py
similarity index 100%
rename from api/migrations/0013_auto_20170116_1304.py
rename to project/api/migrations/0013_auto_20170116_1304.py
diff --git a/api/migrations/0014_auto_20170124_1944.py b/project/api/migrations/0014_auto_20170124_1944.py
similarity index 100%
rename from api/migrations/0014_auto_20170124_1944.py
rename to project/api/migrations/0014_auto_20170124_1944.py
diff --git a/api/migrations/0015_auto_20170331_0710.py b/project/api/migrations/0015_auto_20170331_0710.py
similarity index 100%
rename from api/migrations/0015_auto_20170331_0710.py
rename to project/api/migrations/0015_auto_20170331_0710.py
diff --git a/api/migrations/0016_remove_civiimage_last_modified.py b/project/api/migrations/0016_remove_civiimage_last_modified.py
similarity index 88%
rename from api/migrations/0016_remove_civiimage_last_modified.py
rename to project/api/migrations/0016_remove_civiimage_last_modified.py
index c4669d298..2d3a25401 100644
--- a/api/migrations/0016_remove_civiimage_last_modified.py
+++ b/project/api/migrations/0016_remove_civiimage_last_modified.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import migrations, models
+from django.db import migrations
class Migration(migrations.Migration):
diff --git a/api/migrations/0017_auto_20170331_1233.py b/project/api/migrations/0017_auto_20170331_1233.py
similarity index 100%
rename from api/migrations/0017_auto_20170331_1233.py
rename to project/api/migrations/0017_auto_20170331_1233.py
diff --git a/api/migrations/0018_representative_official_full_name.py b/project/api/migrations/0018_representative_official_full_name.py
similarity index 100%
rename from api/migrations/0018_representative_official_full_name.py
rename to project/api/migrations/0018_representative_official_full_name.py
diff --git a/api/migrations/0019_auto_20170418_0727.py b/project/api/migrations/0019_auto_20170418_0727.py
similarity index 100%
rename from api/migrations/0019_auto_20170418_0727.py
rename to project/api/migrations/0019_auto_20170418_0727.py
diff --git a/api/migrations/0020_auto_20170418_1652.py b/project/api/migrations/0020_auto_20170418_1652.py
similarity index 100%
rename from api/migrations/0020_auto_20170418_1652.py
rename to project/api/migrations/0020_auto_20170418_1652.py
diff --git a/api/migrations/0021_auto_20170418_2111.py b/project/api/migrations/0021_auto_20170418_2111.py
similarity index 100%
rename from api/migrations/0021_auto_20170418_2111.py
rename to project/api/migrations/0021_auto_20170418_2111.py
diff --git a/api/migrations/0022_auto_20170531_1316.py b/project/api/migrations/0022_auto_20170531_1316.py
similarity index 100%
rename from api/migrations/0022_auto_20170531_1316.py
rename to project/api/migrations/0022_auto_20170531_1316.py
diff --git a/api/migrations/0023_auto_20170615_0827.py b/project/api/migrations/0023_auto_20170615_0827.py
similarity index 100%
rename from api/migrations/0023_auto_20170615_0827.py
rename to project/api/migrations/0023_auto_20170615_0827.py
diff --git a/project/api/migrations/0024_remove_civi_links.py b/project/api/migrations/0024_remove_civi_links.py
new file mode 100644
index 000000000..6ad101c17
--- /dev/null
+++ b/project/api/migrations/0024_remove_civi_links.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0023_auto_20170615_0827'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='civi',
+ name='links',
+ ),
+ ]
diff --git a/project/api/migrations/0025_thread_is_draft.py b/project/api/migrations/0025_thread_is_draft.py
new file mode 100644
index 000000000..365f45b0a
--- /dev/null
+++ b/project/api/migrations/0025_thread_is_draft.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0024_remove_civi_links'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='thread',
+ name='is_draft',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/api/migrations/__init__.py b/project/api/migrations/__init__.py
similarity index 100%
rename from api/migrations/__init__.py
rename to project/api/migrations/__init__.py
diff --git a/api/models/__init__.py b/project/api/models/__init__.py
similarity index 100%
rename from api/models/__init__.py
rename to project/api/models/__init__.py
diff --git a/api/models/account.py b/project/api/models/account.py
similarity index 75%
rename from api/models/account.py
rename to project/api/models/account.py
index 0b63fdafe..01b93adc4 100644
--- a/api/models/account.py
+++ b/project/api/models/account.py
@@ -9,7 +9,6 @@
from django.contrib.auth.models import User
from django.utils.deconstruct import deconstructible
from django.core.files.storage import default_storage
-from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import models
from PIL import Image, ImageOps
@@ -17,14 +16,14 @@
from utils.constants import US_STATES
-from hashtag import Hashtag
-from category import Category
-from representative import Representative
+from .hashtag import Hashtag
+from .category import Category
+from .representative import Representative
# Image manipulation constants
-PROFILE_IMAGE_SIZE = (171, 171)
-PROFILE_IMAGE_THUMB_SIZE = (40, 40)
+PROFILE_IMG_SIZE = (171, 171)
+PROFILE_IMG_THUMB_SIZE = (40, 40)
WHITE_BG = (255,255,255)
@@ -36,7 +35,7 @@ def summarize(self, account):
"first_name": account.first_name,
"last_name": account.last_name,
"about_me": account.about_me,
- "location": account.get_location(),
+ "location": account.location,
"history": [Civi.objects.serialize(c) for c in Civi.objects.filter(author_id=account.id).order_by('-created')],
"profile_image": account.profile_image_url,
"followers": self.followers(account),
@@ -60,7 +59,7 @@ def card_summarize(self, account, request_account):
"first_name": account.first_name,
"last_name": account.last_name,
"about_me": account.about_me[:150] + ('' if len(account.about_me) <= 150 else '...'),
- "location": account.get_location(),
+ "location": account.location,
"profile_image": account.profile_image_url,
"follow_state": True if account in request_account.following.all() else False,
"request_account": request_account.first_name
@@ -85,6 +84,7 @@ def __call__(self, instance, filename):
filename = '{}.{}'.format(new_filename, ext)
return os.path.join(self.sub_path, filename)
+
profile_upload_path = PathAndRename('')
@@ -121,7 +121,7 @@ class Account(models.Model):
profile_image_thumb = models.ImageField(upload_to=profile_upload_path, blank=True, null=True)
#custom "row-level" functionality (properties) for account models
- def get_location(self):
+ def _get_location(self):
""" Constructs a CITY, STATE string for locations in the US """
if self.city and self.state:
return '{city}, {state}'.format(city=self.city, state=dict(US_STATES).get(self.state))
@@ -129,6 +129,7 @@ def get_location(self):
return '{state}'.format(state=dict(US_STATES).get(self.state))
else:
return 'NO LOCATION'
+ location = property(_get_location)
def _get_full_name(self):
"Returns the person's full name."
@@ -148,11 +149,25 @@ def _get_profile_image_url(self):
)
if self.profile_image and file_exists:
return self.profile_image.url
- else:
- #NOTE: This default url will probably be changed later
- return "/static/img/no_image_md.png",
+
+ #NOTE: This default url will probably be changed later
+ return "/static/img/no_image_md.png"
profile_image_url = property(_get_profile_image_url)
+ def _get_profile_image_thumb_url(self):
+ """ Return placeholder profile image if user didn't upload one"""
+
+ file_exists = default_storage.exists(
+ os.path.join(settings.MEDIA_ROOT, self.profile_image_thumb.name)
+ )
+ if self.profile_image_thumb and file_exists:
+ return self.profile_image_thumb.url
+
+ #NOTE: This default url will probably be changed later
+ return "/static/img/no_image_md.png"
+
+ profile_image_thumb_url = property(_get_profile_image_thumb_url)
+
def __init__(self, *args, **kwargs):
super(Account, self).__init__(*args, **kwargs)
@@ -163,34 +178,61 @@ def save(self, *args, **kwargs):
if self.profile_image:
self.resize_profile_image()
+ self.full_account = self.is_full_account()
+
super(Account, self).save(*args, **kwargs)
#
#
+
def resize_profile_image(self):
+ """
+ Resizes and crops the user uploaded image and creates a thumbnail version of it
+ """
profile_image_field = self.profile_image
image_file = StringIO.StringIO(profile_image_field.read())
profile_image = Image.open(image_file)
profile_image.load()
# Resize image
- profile_image = ImageOps.fit(profile_image, PROFILE_IMAGE_SIZE, Image.ANTIALIAS, centering=(0.5, 0.5))
+ profile_image = ImageOps.fit(profile_image, PROFILE_IMG_SIZE, Image.ANTIALIAS)
+
+ # TODO: Check if GIF to allow GIF uploads. Get 1st frame if so
+
# Convert to JPG image format with white background
if profile_image.mode not in ('L', 'RGB'):
- white_bg_img = Image.new("RGB", PROFILE_IMAGE_SIZE, WHITE_BG)
+ white_bg_img = Image.new("RGB", PROFILE_IMG_SIZE, WHITE_BG)
white_bg_img.paste(profile_image, mask=profile_image.split()[3])
profile_image = white_bg_img
- # Create thumb
+ # Save new cropped image
tmp_image_file = StringIO.StringIO()
profile_image.save(tmp_image_file, 'JPEG', quality=90)
tmp_image_file.seek(0)
- self.profile_image = InMemoryUploadedFile(tmp_image_file, 'ImageField', self.profile_image.name, 'image/jpeg', tmp_image_file.len, None)
-
-
- # # Make a Thumbnail Image for the new resized image
+ self.profile_image = InMemoryUploadedFile(
+ tmp_image_file,
+ 'ImageField',
+ self.profile_image.name,
+ 'image/jpeg',
+ tmp_image_file.len,
+ None
+ )
+ # Make a Thumbnail Image for the new resized image
thumb_image = profile_image.copy()
- thumb_image.thumbnail(PROFILE_IMAGE_THUMB_SIZE, resample=Image.ANTIALIAS)
+ thumb_image.thumbnail(PROFILE_IMG_THUMB_SIZE, resample=Image.ANTIALIAS)
tmp_image_file = StringIO.StringIO()
thumb_image.save(tmp_image_file, 'JPEG', quality=90)
tmp_image_file.seek(0)
- self.profile_image_thumb = InMemoryUploadedFile(tmp_image_file, 'ImageField', self.profile_image.name, 'image/jpeg', tmp_image_file.len, None)
+ self.profile_image_thumb = InMemoryUploadedFile(
+ tmp_image_file,
+ 'ImageField',
+ self.profile_image.name,
+ 'image/jpeg',
+ tmp_image_file.len,
+ None
+ )
+
+ def is_full_account(self):
+ if self.first_name and self.last_name and self.longitude and self.latitude:
+ return True
+ else:
+ return False
diff --git a/api/models/activity.py b/project/api/models/activity.py
similarity index 93%
rename from api/models/activity.py
rename to project/api/models/activity.py
index 0894d644f..1e4bc09bc 100644
--- a/api/models/activity.py
+++ b/project/api/models/activity.py
@@ -1,7 +1,7 @@
from django.db import models
-from account import Account
-from civi import Civi
-from thread import Thread
+from .account import Account
+from .civi import Civi
+from .thread import Thread
class ActivityManager(models.Manager):
def votes(self, civi_id):
diff --git a/api/models/bill.py b/project/api/models/bill.py
similarity index 100%
rename from api/models/bill.py
rename to project/api/models/bill.py
diff --git a/api/models/category.py b/project/api/models/category.py
similarity index 100%
rename from api/models/category.py
rename to project/api/models/category.py
diff --git a/api/models/civi.py b/project/api/models/civi.py
similarity index 92%
rename from api/models/civi.py
rename to project/api/models/civi.py
index c2e79e4ca..74597b8ce 100644
--- a/api/models/civi.py
+++ b/project/api/models/civi.py
@@ -1,8 +1,8 @@
from django.db import models
-from account import Account
-from thread import Thread
-from bill import Bill
-from hashtag import Hashtag
+from .account import Account
+from .thread import Thread
+from .bill import Bill
+from .hashtag import Hashtag
import random as r
import os, json, datetime, math, uuid
from django.core.serializers.json import DjangoJSONEncoder
@@ -38,7 +38,7 @@ def serialize(self, civi, filter=None):
"votes": civi.votes(),
"id": civi.id,
"thread_id": civi.thread.id
- }
+ }
if filter and filter in data:
return json.dumps({filter: data[filter]})
@@ -51,7 +51,7 @@ def serialize_s(self, civi, filter=None):
"body": civi.body,
"author": dict(
username=civi.author.user.username,
- profile_image= civi.author.profile_image.url if civi.author.profile_image else "/media/profile/default.png",
+ profile_image=civi.author.profile_image.url if civi.author.profile_image else "/media/profile/default.png",
first_name=civi.author.first_name,
last_name=civi.author.last_name
),
@@ -62,7 +62,7 @@ def serialize_s(self, civi, filter=None):
"id": civi.id,
"thread_id": civi.thread.id,
"links": [c for c in civi.linked_civis.all().values_list('id', flat=True)]
- }
+ }
if filter and filter in data:
return json.dumps({filter: data[filter]})
@@ -96,9 +96,6 @@ class Civi(models.Model):
)
c_type = models.CharField(max_length=31, default='problem', choices=c_CHOICES)
- # sources = ArrayField(models.CharField(max_length=127, blank=True), default=[], blank=True)
- # attachments
-
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
@@ -113,7 +110,7 @@ def votes(self):
'total': act_votes.count() - act_votes.filter(activity_type='vote_neutral').count(),
'votes_vneg': act_votes.filter(activity_type='vote_vneg').count(),
'votes_neg': act_votes.filter(activity_type='vote_neg').count(),
- 'votes_neutral': act_votes.filter(activity_type='vote_neutral').count(),
+ 'votes_neutral': act_votes.filter(activity_type='vote_neutral').count(),
'votes_pos': act_votes.filter(activity_type='vote_pos').count(),
'votes_vpos': act_votes.filter(activity_type='vote_vpos').count()
}
@@ -121,6 +118,7 @@ def votes(self):
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
+
def _get_created_date_str(self):
d = self.created
return "{0} {1}, {2}".format(month_name[d.month], d.day, d.year)
@@ -131,7 +129,7 @@ def score(self, request_acct_id=None):
D = -1
A = 1
SA = 2
- owner_id = self.author
+
post_time = self.created
current_time = datetime.datetime.now()
@@ -153,7 +151,7 @@ def score(self, request_acct_id=None):
if x > 0:
v = votes['total'] if votes['total'] > 1 else 2
#step3 - A X*Log10V+Y + F + (##/T) = Rank Value
- rank = x * math.log10(v) *amp + y + f + g/t
+ rank = x * math.log10(v) * amp + y + f + g/t
elif x == 0:
@@ -163,7 +161,7 @@ def score(self, request_acct_id=None):
v = votes['total'] if votes['total'] > 1 else 2
#step3 - C
if abs(x)/v <= 5:
- rank = abs(x) * math.log10(v) *amp + y + f + g/t
+ rank = abs(x) * math.log10(v) * amp + y + f + g/t
else:
rank = x * math.log10(v) * amp + y + f + g/t
return rank
@@ -187,7 +185,7 @@ def dict_with_score(self, req_acct_id=None):
# Not Implemented Yet
"hashtags": [],
"attachments": [{'id': img.id, 'url': img.image_url} for img in self.images.all()],
- }
+ }
if req_acct_id:
data['score'] = self.score(req_acct_id)
@@ -205,6 +203,7 @@ def __call__(self, instance, filename):
filename = '{}.{}'.format(new_filename, ext)
return os.path.join(self.sub_path, filename)
+
image_upload_path = PathAndRename('')
class CiviImageManager(models.Manager):
diff --git a/api/models/fact.py b/project/api/models/fact.py
similarity index 100%
rename from api/models/fact.py
rename to project/api/models/fact.py
diff --git a/api/models/hashtag.py b/project/api/models/hashtag.py
similarity index 100%
rename from api/models/hashtag.py
rename to project/api/models/hashtag.py
diff --git a/api/models/invitation.py b/project/api/models/invitation.py
similarity index 97%
rename from api/models/invitation.py
rename to project/api/models/invitation.py
index 5426c59ba..a870fb526 100644
--- a/api/models/invitation.py
+++ b/project/api/models/invitation.py
@@ -3,8 +3,6 @@
"""
from django.db import models
from django.contrib.auth.models import User
-from datetime import datetime
-from django.utils import formats
class InvitationManager(models.Manager):
"""
diff --git a/api/models/notification.py b/project/api/models/notification.py
similarity index 91%
rename from api/models/notification.py
rename to project/api/models/notification.py
index 285d6c151..543c5f330 100644
--- a/api/models/notification.py
+++ b/project/api/models/notification.py
@@ -1,7 +1,7 @@
from django.db import models
-from account import Account
-from civi import Civi
-from thread import Thread
+from .account import Account
+from .civi import Civi
+from .thread import Thread
class Notification(models.Model):
diff --git a/api/models/rationale.py b/project/api/models/rationale.py
similarity index 89%
rename from api/models/rationale.py
rename to project/api/models/rationale.py
index 4635941a9..e9415d72e 100644
--- a/api/models/rationale.py
+++ b/project/api/models/rationale.py
@@ -1,7 +1,7 @@
from django.db import models
-from bill import Bill
-from representative import Representative
-from vote import Vote
+from .bill import Bill
+from .representative import Representative
+from .vote import Vote
class Rationale(models.Model):
@@ -17,6 +17,6 @@ class Rationale(models.Model):
votes_neutral = models.IntegerField(default=0)
votes_pos = models.IntegerField(default=0)
votes_vpos = models.IntegerField(default=0)
-
+
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
diff --git a/api/models/rebuttal.py b/project/api/models/rebuttal.py
similarity index 75%
rename from api/models/rebuttal.py
rename to project/api/models/rebuttal.py
index 14d7b4867..9512e72ae 100644
--- a/api/models/rebuttal.py
+++ b/project/api/models/rebuttal.py
@@ -1,8 +1,7 @@
from django.db import models
-from account import Account
-from response import Response
-# from django.contrib.postgres.fields import ArrayField
+from .account import Account
+from .response import Response
class Rebuttal(models.Model):
author = models.ForeignKey(Account, default=None, null=True)
@@ -10,8 +9,6 @@ class Rebuttal(models.Model):
body = models.TextField(max_length=1023)
- # sources = ArrayField(models.CharField(max_length=127, blank=True), default=[], blank=True)
-
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
diff --git a/api/models/representative.py b/project/api/models/representative.py
similarity index 94%
rename from api/models/representative.py
rename to project/api/models/representative.py
index 7b9814d6d..e64871a9a 100644
--- a/api/models/representative.py
+++ b/project/api/models/representative.py
@@ -1,5 +1,4 @@
from django.db import models
-from django.conf import settings
from utils.constants import US_STATES
class RepresentativeManager(models.Manager):
@@ -7,8 +6,6 @@ def get_reps():
return
class Representative(models.Model):
- # account = models.ForeignKey(Account, default=None, null=True)
-
first_name = models.CharField(max_length=63, blank=False)
last_name = models.CharField(max_length=63, blank=False)
official_full_name = models.CharField(max_length=127, blank=True, null=True)
diff --git a/api/models/response.py b/project/api/models/response.py
similarity index 76%
rename from api/models/response.py
rename to project/api/models/response.py
index b3965b165..53582172b 100644
--- a/api/models/response.py
+++ b/project/api/models/response.py
@@ -1,8 +1,7 @@
from django.db import models
-from account import Account
-from civi import Civi
-# from django.contrib.postgres.fields import ArrayField
+from .account import Account
+from .civi import Civi
class Response(models.Model):
author = models.ForeignKey(Account, default=None, null=True)
@@ -11,8 +10,6 @@ class Response(models.Model):
title = models.CharField(max_length=127)
body = models.TextField(max_length=2047)
- # sources = ArrayField(models.CharField(max_length=127, blank=True), default=[], blank=True)
-
votes_vneg = models.IntegerField(default=0)
votes_neg = models.IntegerField(default=0)
votes_neutral = models.IntegerField(default=0)
diff --git a/api/models/thread.py b/project/api/models/thread.py
similarity index 93%
rename from api/models/thread.py
rename to project/api/models/thread.py
index a3f51eb87..909f96579 100644
--- a/api/models/thread.py
+++ b/project/api/models/thread.py
@@ -1,10 +1,10 @@
from django.db import models
-from account import Account
-from category import Category
-from fact import Fact
-from hashtag import Hashtag
+from .account import Account
+from .category import Category
+from .fact import Fact
+from .hashtag import Hashtag
from calendar import month_name
-import os, json, uuid
+import os, uuid
from django.utils.deconstruct import deconstructible
from django.core.files.storage import default_storage
from django.conf import settings
@@ -54,6 +54,7 @@ def __call__(self, instance, filename):
filename = '{}.{}'.format(new_filename, ext)
return os.path.join(self.sub_path, filename)
+
image_upload_path = PathAndRename('')
class Thread(models.Model):
@@ -85,6 +86,9 @@ def _get_image_url(self): #TODO: move this to utils
created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_modified = models.DateTimeField(auto_now=True, blank=True, null=True)
+ # Allow draft stage threads (default to True)
+ is_draft = models.BooleanField(default=True)
+
num_views = models.IntegerField(default=0)
num_civis = models.IntegerField(default=0)
num_solutions = models.IntegerField(default=0)
diff --git a/api/models/vote.py b/project/api/models/vote.py
similarity index 89%
rename from api/models/vote.py
rename to project/api/models/vote.py
index 965f6c222..54152c216 100644
--- a/api/models/vote.py
+++ b/project/api/models/vote.py
@@ -1,6 +1,6 @@
from django.db import models
-from bill import Bill
-from representative import Representative
+from .bill import Bill
+from .representative import Representative
class Vote(models.Model):
diff --git a/api/read.py b/project/api/read.py
similarity index 61%
rename from api/read.py
rename to project/api/read.py
index a840ad99c..c24ee055d 100644
--- a/api/read.py
+++ b/project/api/read.py
@@ -1,52 +1,11 @@
from django.http import JsonResponse, HttpResponseBadRequest
from models import Account, Thread, Civi, Representative, Category, Activity
# Topic, Attachment, Category, Civi, Comment, Hashtag,
-from django.db.models import Q
from django.contrib.auth.models import User
-from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict
from utils import json_response
-import json
-# from utils.custom_decorators import require_post_params
-from legislation import sunlightapi as sun
-
-import tasks
-# # Create your views here.
-# @require_post_params(params=['id'])
-# def topTen(request):
-# '''
-# Given an topic ID, returns the top ten Civis of type Issue
-# (the chain heads)
-# '''
-# topic_id = request.POST.get('id', -1)
-# try:
-# topic = Topic.objects.get(id=int(topic_id))
-# return JsonResponse({'result':Civi.objects.block(topic)})
-# except Topic.DoesNotExist as e:
-# return HttpResponseBadRequest(reason=str(e))
-#
-# def getCategories(request):
-# '''
-# Returns to user list of all Categories
-# '''
-# result = [{'id': c.id, 'name': c.name} for c in Category.objects.all()]
-# return JsonResponse({"result":result})
-#
-# @require_post_params(params=['id'])
-# def getTopics(request):
-# '''
-# Takes in a category ID, returns a list of topics under the category.
-# '''
-# category_id = request.POST.get('id', -1)
-#
-# try:
-# Category.objects.get(id=category_id)
-# except Category.DoesNotExist as e:
-# return HttpResponseBadRequest(reason=str(e))
-#
-# result = [{'id':a.id, 'topic': a.topic, 'bill': a.bill} for a in Topic.objects.filter(category_id=category_id)]
-# return JsonResponse({"result":result})
+from legislation import sunlightapi as sun
def get_user(request, user):
try:
@@ -73,10 +32,8 @@ def get_profile(request, user):
u = User.objects.get(username=user)
a = Account.objects.get(user=u)
result = Account.objects.summarize(a)
- # result = {}
result['representatives'] = []
- # result['issues'] = ['''{"category": "category", "issue":"Example Issue that the User probably cares about" }''']*20
result['issues'] = []
voted_solutions = Activity.objects.filter(account=a.id, civi__c_type="solution", activity_type__contains="pos")
@@ -110,7 +67,7 @@ def get_profile(request, user):
result['issues'].append(my_issue_item)
result['representatives'] = []
- rep_ids = sun.get_legislator_ids_by_lat_long(a.latitude, a.longitude)
+ rep_ids = []
for bio_id in rep_ids:
rep = Representative.objects.get(bioguideID=bio_id)
@@ -137,7 +94,6 @@ def get_rep(request, rep_id):
result = Account.objects.summarize(a)
result['representatives'] = []
- # result['issues'] = ['''{"category": "category", "issue":"Example Issue that the User probably cares about" }''']*20
if request.user.username != user:
ra = Account.objects.get(user=request.user)
if user in ra.following.all():
@@ -151,19 +107,8 @@ def get_rep(request, rep_id):
def get_feed(request):
try:
- a = Account.objects.get(user=request.user)
- all_categories = Category.objects.values_list('id', flat=True)
- user_categories = list(a.categories.values_list('id', flat=True)) or all_categories
-
- # feed_threads = [Thread.objects.summarize(t) for t in Thread.objects.filter_by_category(user_categories).order_by('-created')]
- # top5_threads = Thread.objects.all().order_by('-views')[:5].values('id', 'title')
feed_threads = [Thread.objects.summarize(t) for t in Thread.objects.order_by('-created')]
- # data = {
- # 'threads': feed_threads,
- # # 'trending_threads': top5_threads
- # }
-
return json_response(feed_threads)
except Exception as e:
@@ -174,9 +119,6 @@ def get_thread(request, thread_id):
t = Thread.objects.get(id=thread_id)
civis = Civi.objects.filter(thread_id=thread_id)
req_a = Account.objects.get(user=request.user)
- # problems = civis.filter(c_type='problem')
- # causes = civis.filter(c_type='cause')
- # solutions = civis.filter(c_type='solution')
#TODO: move order by to frontend or accept optional arg
c = civis.order_by('-created')
@@ -184,32 +126,6 @@ def get_thread(request, thread_id):
c_data = [Civi.objects.serialize_s(ci) for ci in c]
for idx, item in enumerate(c_data):
problems[idx]['score'] = c_scores[idx]
- problems = sorted(problems, key=lambda x: x['score'], reverse=True)
- # for idx, item in enumerate(problems):
- # problems[idx] = json.dumps(item, cls=DjangoJSONEncoder)
- #
- # c = civis.filter(c_type='cause').order_by('-created')
- # c_scores = [ci.score(req_a.id) for ci in c]
- # causes = [Civi.objects.serialize_s(ci) for ci in c]
- # for idx, item in enumerate(causes):
- # causes[idx]['score'] = c_scores[idx]
- # causes = sorted(causes, key=lambda x: x['score'], reverse=True)
- # for idx, item in enumerate(causes):
- # causes[idx] = json.dumps(item, cls=DjangoJSONEncoder)
- #
- # c = civis.filter(c_type='solution').order_by('-created')
- # c_scores = [ci.score(req_a.id) for ci in c]
- # solutions = [Civi.objects.serialize_s(ci) for ci in c]
- # for idx, item in enumerate(solutions):
- # solutions[idx]['score'] = c_scores[idx]
- # solutions = sorted(solutions, key=lambda x: x['score'], reverse=True)
- # for idx, item in enumerate(solutions):
- # solutions[idx] = json.dumps(item, cls=DjangoJSONEncoder)
-
- # problems = [Civi.objects.serialize(c) for c in civis.filter(c_type='problem').order_by('-votes_vpos', '-votes_pos')]
- # problems =
- # causes = [Civi.objects.serialize(c) for c in civis.filter(c_type='cause').order_by('-votes_vpos', '-votes_pos')]
- # solutions = [Civi.objects.serialize(c) for c in civis.filter(c_type='solution').order_by('-votes_vpos', '-votes_pos')]
data = {
'title': t.title,
@@ -217,15 +133,12 @@ def get_thread(request, thread_id):
'hashtags': t.hashtags.all().values(),
'author': {
'username': t.author.user.username,
- 'profile_image': t.author.profile_image.url if t.author.profile_image else "/media/profile/default.png",
+ 'profile_image': t.author.profile_image.url if t.author.profile_image else "/media/profile/default.png",
'first_name': t.author.first_name,
'last_name': t.author.last_name
},
'category': model_to_dict(t.category),
'created': t.created,
- # 'problems': problems,
- # 'causes': causes,
- # 'solutions': solutions,
'contributors': [Account.objects.chip_summarize(a) for a in Account.objects.filter(pk__in=civis.distinct('author').values_list('author', flat=True))],
'num_civis': t.num_civis,
'num_views': t.num_views,
diff --git a/api/tasks.py b/project/api/tasks.py
similarity index 96%
rename from api/tasks.py
rename to project/api/tasks.py
index 56d1a6c13..42adc3175 100644
--- a/api/tasks.py
+++ b/project/api/tasks.py
@@ -10,9 +10,8 @@
from __future__ import absolute_import
from django.conf import settings
-from django.core import mail
from celery import shared_task
-from django.core.mail import send_mail, send_mass_mail
+from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.core.mail import get_connection, EmailMultiAlternatives
diff --git a/api/urls.py b/project/api/urls.py
similarity index 64%
rename from api/urls.py
rename to project/api/urls.py
index c5f146452..d650e7e22 100644
--- a/api/urls.py
+++ b/project/api/urls.py
@@ -3,12 +3,6 @@
#TODO: RESTful API - http://www.django-rest-framework.org/
urlpatterns = [
- # url(r'^getcivi$', read.getCivi, name='civi'),
- # url(r'^topten$', read.topTen, name='example'),
- # url(r'^topics$', read.getTopics, name='get topics'),
- # url(r'^categories$', read.getCategories, name='get categories'),
- # url(r'^useridbyusername$', read.getIdByUsername, name='get id by username'),
-
url(r'^account_data/(?P[-\w]+)/$', read.get_user, name='get user'),
url(r'^account_profile/(?P[-\w]+)/$', read.get_profile, name='get profile'),
url(r'^account_card/(?P[-\w]+)$', read.get_card, name='get card'),
@@ -24,9 +18,6 @@
url(r'^edit_civi/$', write.editCivi, name='edit civi'),
url(r'^delete_civi/$', write.deleteCivi, name='delete civi'),
- # url(r'^getblock$', read.getBlock, name='get block'),
- # url(r'^creategroup$', write.createGroup, name='add group'),
- # url(r'^createcivi$', write.createCivi, name='add civi'),
url(r'^invite/$',write.invite, name='invite users'),
url(r'^edituser/$',write.editUser, name='edit user'),
url(r'^upload_profile/$',write.uploadProfileImage, name='upload profile'),
@@ -36,13 +27,4 @@
url(r'^follow/$', write.requestFollow, name='follow user'),
url(r'^unfollow/$', write.requestUnfollow, name='unfollow user'),
url(r'^edit_user_categories/$', write.editUserCategories, name='edit user categories'),
-
-
- # url(r'^requestfriend$',write.requestFriend, name='request friend'),
- # url(r'^acceptfriend$',write.acceptFriend, name='accept friend'),
- # url(r'^removefriend$',write.removeFriend, name='remove friend'),
- # url(r'^followgroup$',write.followGroup, name='follow group'),
- # url(r'^unfollowgroup$', write.unfollowGroup, name='unfollow group'),
- # url(r'^pincivi$', write.pinCivi, name='pin civi'),
- # url(r'^unpincivi$', write.unpinCivi, name='unpin civi'),
-]
+ ]
diff --git a/api/write.py b/project/api/write.py
similarity index 76%
rename from api/write.py
rename to project/api/write.py
index 745780cf2..d0cecff23 100644
--- a/api/write.py
+++ b/project/api/write.py
@@ -1,27 +1,25 @@
-import os, sys, json, pdb, random, hashlib, urllib2, pprint, urllib, PIL
-from models import Account, Category, Civi, CiviImage, Hashtag, Activity, Invitation
+import json, PIL, urllib, uuid
+
+from notifications.signals import notify
+
+# django packages
from django.contrib.auth.models import User
from django.http import JsonResponse, HttpResponse, HttpResponseServerError, HttpResponseForbidden, HttpResponseBadRequest
-from utils.custom_decorators import require_post_params
-from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
-from django.conf import settings
-from utils.constants import US_STATES
-# from django.db.models import Q
-from api.forms import UpdateProfileImage
-from django.core.files import File # need this for image file handling
-from django.core.files.base import ContentFile
-from api.models import Thread
-from channels import Group as channels_Group
-from utils.custom_decorators import require_post_params
-from django.http import JsonResponse
+from django.core.files import File # need this for image file handling
from django.contrib.auth.decorators import login_required
-from notifications.signals import notify
from django.contrib.auth.decorators import user_passes_test
from django.contrib.sites.shortcuts import get_current_site
-from api.tasks import send_email, send_mass_email
-import uuid
+
+
+# civi packages
+from api.forms import UpdateProfileImage
+from api.models import Thread
+from api.tasks import send_mass_email
+from models import Account, Activity, Category, Civi, CiviImage, Invitation
+from utils.custom_decorators import require_post_params
+from utils.constants import US_STATES
+from utils.custom_decorators import require_post_params
@login_required
@@ -38,68 +36,15 @@ def new_thread(request):
if state:
new_thread_data['state'] = state
+ is_draft = request.POST.get('is_draft', True)
+ if is_draft:
+ new_thread_data['is_draft'] = is_draft
+
new_t = Thread(**new_thread_data)
new_t.save()
return JsonResponse({'data': 'success', 'thread_id' : new_t.pk})
-# @login_required
-# @transaction.atomic
-# def update_profile(request):
-# if request.method == 'POST':
-# user_form = UserForm(request.POST, instance=request.user)
-# profile_form = ProfileForm(request.POST, instance=request.user.profile)
-# if user_form.is_valid() and profile_form.is_valid():
-# user_form.save()
-# profile_form.save()
-# messages.success(request, _('Your profile was successfully updated!'))
-# return redirect('settings:profile')
-# else:
-# messages.error(request, _('Please correct the error below.'))
-# else:
-# user_form = UserForm(instance=request.user)
-# profile_form = ProfileForm(instance=request.user.profile)
-# return render(request, 'profiles/profile.html', {
-# 'user_form': user_form,
-# 'profile_form': profile_form
-# })
-# @login_required
-# @require_post_params(params=['title', 'description'])
-# def createGroup(request):
-# '''
-# USAGE:
-# create a civi Group responsible for creating and managing civi content.
-# Please validate file uploads as valid images on the frontend.
-#
-# File Uploads:
-# profile (optional)
-# cover (optional)
-#
-# Text POST:
-# title
-# description
-#
-# :returns: (200, ok, group_id) (500, error)
-# '''
-# pi = request.FILES.get('profile', False)
-# ci = request.FILES.get('cover', False)
-# title = request.POST.get(title, '')
-# data = {
-# "owner": Account.objects.get(user=request.user),
-# "title": title,
-# "description": request.POST.get('description',''),
-# "profile_image": writeImage('profile', pi, title),
-# "cover_image": writeImage('cover', ci, title)
-# }
-#
-# try:
-# group = Group(**data)
-# group.save()
-# account.groups.add(group)
-# return JsonResponse({'result':group.id})
-# except Exception as e:
-# return HttpResponseServerError(reason=e)
-#
@login_required
@require_post_params(params=['title', 'body', 'c_type', 'thread_id'])
def createCivi(request):
@@ -123,16 +68,6 @@ def createCivi(request):
try:
civi = Civi(**data)
civi.save()
- # hashtags = request.POST.get('hashtags', '')
- # split = [x.strip() for x in hashtags.split(',')]
- # for str in split:
- # if not Hashtag.objects.filter(title=str).exists():
- # hash = Hashtag(title=str)
- # hash.save()
- # else:
- # hash = Hashtag.objects.get(title=str)
- #
- # civi.hashtags.add(hash.id)
links = request.POST.getlist('links[]', '')
if links:
for civi_id in links:
@@ -142,8 +77,6 @@ def createCivi(request):
# If response
related_civi = request.POST.get('related_civi', '')
if related_civi:
- # parent_civi = Civi.objects.get(id=related_civi)
- # parent_civi.links.add(civi)
parent_civi = Civi.objects.get(id=related_civi)
parent_civi.responses.add(civi)
@@ -165,9 +98,6 @@ def createCivi(request):
"command": "add",
"data": json.dumps(civi.dict_with_score(a.id)),
}
- # channels_Group("thread-%s" % thread_id).send({
- # "text": json.dumps(data),
- # })
for act in accounts:
if act.user.username != request.user.username:
@@ -197,10 +127,13 @@ def rateCivi(request):
rating = request.POST.get('rating', '')
account = Account.objects.get(user=request.user)
- c = Civi.objects.get(id=civi_id)
+ voted_civi = Civi.objects.get(id=civi_id)
+
+ if voted_civi.thread.is_draft:
+ return HttpResponseServerError(reason=str("Cannot vote on a civi that is in a thread still in draft mode"))
try:
- prev_act = Activity.objects.get(civi=c, account=account)
+ prev_act = Activity.objects.get(civi=voted_civi, account=account)
except Activity.DoesNotExist:
prev_act = None
@@ -209,28 +142,28 @@ def rateCivi(request):
activity_data = {
'account': account,
- 'thread': c.thread,
- 'civi': c,
+ 'thread': voted_civi.thread,
+ 'civi': voted_civi,
}
if rating == "vneg":
- c.votes_vneg = c.votes_vneg + 1
+ voted_civi.votes_vneg = voted_civi.votes_vneg + 1
vote_val = 'vote_vneg'
elif rating == "neg":
- c.votes_neg = c.votes_neg + 1
+ voted_civi.votes_neg = voted_civi.votes_neg + 1
vote_val = 'vote_neg'
elif rating == "neutral":
- c.votes_neutral = c.votes_neutral + 1
+ voted_civi.votes_neutral = voted_civi.votes_neutral + 1
vote_val = 'vote_neutral'
elif rating == "pos":
- c.votes_pos = c.votes_pos + 1
+ voted_civi.votes_pos = voted_civi.votes_pos + 1
vote_val = 'vote_pos'
elif rating == "vpos":
# c.votes_vpos = c.votes_vpos + 1
vote_val = 'vote_vpos'
activity_data['activity_type'] = vote_val
- c.save()
+ voted_civi.save()
if prev_act:
prev_act.activity_type = vote_val
@@ -328,47 +261,64 @@ def deleteCivi(request):
@login_required
def editThread(request):
+ thread_id = request.POST.get('thread_id')
+
+ if not thread_id:
+ return HttpResponseBadRequest(reason="Invalid Thread Reference")
+
+ # Change Publish State to True
+ is_draft = request.POST.get('is_draft', True)
+ if is_draft in ["false", "False"] or not is_draft:
+ draft_thread = Thread.objects.get(id=thread_id)
+ if request.user.id is not draft_thread.author_id:
+ return HttpResponseForbidden("Only the original creator can publish the draft")
+
+ draft_thread.is_draft = False
+
+ try:
+ draft_thread.save()
+ except Exception as e:
+ return HttpResponseServerError(reason=str(e))
+
+ return JsonResponse({'data': "remove from draft"})
+
+ title = request.POST.get('title')
+ summary = request.POST.get('summary')
+ category_id = request.POST.get('category_id')
+ level = request.POST.get('level')
+ state = request.POST.get('state')
+
try:
- thread_id = request.POST.get('thread_id')
- title = request.POST.get('title')
- summary = request.POST.get('summary')
- category_id = request.POST.get('category_id')
- level = request.POST.get('level')
- state = request.POST.get('state')
- if thread_id:
-
-
- t = Thread.objects.get(id=thread_id)
- category_id = request.POST.get('category_id')
- if request.user.username != t.author.user.username:
- return HttpResponseBadRequest(reason="No Edit Rights")
-
- t.title = title
- t.summary = summary
- t.category_id = category_id
- t.level = level
- t.state = state
- t.save()
-
- return_data = {
- 'thread_id': thread_id,
- 'title': t.title,
- 'summary': t.summary,
- "category": {
- "id": t.category.id,
- "name": t.category.name
- },
- "level": t.level,
- "state": t.state if t.level == "state" else "",
- "location": t.level if not t.state else dict(US_STATES).get(t.state),
- }
- return JsonResponse({'data': return_data})
- else:
- return HttpResponseBadRequest(reason="Invalid Thread Reference")
+ req_edit_thread = Thread.objects.get(id=thread_id)
+ category_id = request.POST.get('category_id')
+ if request.user.username != req_edit_thread.author.user.username:
+ return HttpResponseBadRequest("No Edit Rights")
+
+ req_edit_thread.title = title
+ req_edit_thread.summary = summary
+ req_edit_thread.category_id = category_id
+ req_edit_thread.level = level
+ req_edit_thread.state = state
+ req_edit_thread.save()
except Exception as e:
return HttpResponseServerError(reason=str(e))
+ return_data = {
+ 'thread_id': thread_id,
+ 'title': req_edit_thread.title,
+ 'summary': req_edit_thread.summary,
+ "category": {
+ "id": req_edit_thread.category.id,
+ "name": req_edit_thread.category.name
+ },
+ "level": req_edit_thread.level,
+ "state": req_edit_thread.state if req_edit_thread.level == "state" else "",
+ "location": req_edit_thread.level if not req_edit_thread.state else dict(US_STATES).get(req_edit_thread.state),
+ }
+ return JsonResponse({'data': return_data})
+
+
@login_required
def uploadphoto(request):
pass
@@ -398,17 +348,19 @@ def editUser(request):
"zip_code": r.get('zip_code', account.zip_code),
"longitude": r.get('longitude', account.longitude),
"latitude": r.get('latitude', account.latitude),
- "full_account": r.get('full_account', account.full_account)
}
- try:
- Account.objects.filter(id=account.id).update(**data)
- account.refresh_from_db()
- return JsonResponse(Account.objects.summarize(account))
+ try:
+ account.__dict__.update(data)
+ account.save()
except Exception as e:
return HttpResponseServerError(reason=str(e))
+ account.refresh_from_db()
+
+ return JsonResponse(Account.objects.summarize(account))
+
@login_required
def uploadProfileImage(request):
if request.method == 'POST':
@@ -431,6 +383,8 @@ def uploadProfileImage(request):
}
return JsonResponse(response, status=400)
+ request.session["login_user_image"] = account.profile_image_thumb_url
+
response = {
'profile_image': account.profile_image_url
}
@@ -462,7 +416,7 @@ def clearProfileImage(request):
account.save()
return HttpResponse('Image Deleted')
- except Exception as e:
+ except Exception:
return HttpResponseServerError(reason=str(default))
else:
return HttpResponseForbidden('allowed only via POST')
@@ -496,10 +450,10 @@ def uploadCiviImage(request):
data = {
"attachments": [{'id': img.id, 'url': img.image_url} for img in c.images.all()],
}
- return JsonResponse(data)
+ return JsonResponse(data)
except Exception as e:
- return HttpResponseServerError(reason=(str(e)+ civi_id + str(request.FILES)))
+ return HttpResponseServerError(reason=(str(e) + civi_id + str(request.FILES)))
else:
return HttpResponseForbidden('allowed only via POST')
@@ -546,39 +500,13 @@ def uploadThreadImage(request):
data = {
'image': thread.image_url
}
- return JsonResponse(data)
+ return JsonResponse(data)
except Exception as e:
return HttpResponseServerError(reason=(str(e)))
else:
return HttpResponseForbidden('allowed only via POST')
-# @login_required
-# @require_post_params(params=['friend'])
-# def requestFollow(request):
-# '''
-# USAGE:
-# Takes in a user_id and sends your id to the users friend_requests list. No join
-# is made on accounts until user accepts friend request on other end.
-#
-# Text POST:
-# friend
-#
-# :return: (200, okay) (400, error) (500, error)
-# '''
-# try:
-# account = Account.objects.get(user=request.user)
-# friend = Account.objects.get(id=request.POST.get('friend', -1))
-# if account.id in friend.friend_requests:
-# raise Exception("Request has already been sent to user")
-#
-# friend.friend_requests += [int(account.id)]
-# friend.save()
-# return HttpResponse()
-# except Account.DoesNotExist as e:
-# return HttpResponseBadRequest(reason=str(e))
-# except Exception as e:
-# return HttpResponseServerError(reason=str(e))
-#
+
@login_required
@require_post_params(params=['target'])
def requestFollow(request):
@@ -732,9 +660,6 @@ def invite(request):
email_messages.append(email_context)
new_invitation = Invitation(**data)
- # new_invitation.host_user = user
- # new_invitation.invitee_email = email
- # new_invitation.verification_code = ""
new_invitation.save()
if email_messages:
diff --git a/app.json b/project/app.json
similarity index 100%
rename from app.json
rename to project/app.json
diff --git a/authentication/__init__.py b/project/authentication/__init__.py
similarity index 100%
rename from authentication/__init__.py
rename to project/authentication/__init__.py
diff --git a/authentication/admin.py b/project/authentication/admin.py
similarity index 100%
rename from authentication/admin.py
rename to project/authentication/admin.py
diff --git a/authentication/authentication.py b/project/authentication/authentication.py
similarity index 96%
rename from authentication/authentication.py
rename to project/authentication/authentication.py
index af89e6456..ba4ba2400 100644
--- a/authentication/authentication.py
+++ b/project/authentication/authentication.py
@@ -1,22 +1,17 @@
from django.contrib.auth.models import User
-from django.db import IntegrityError
-from django.core.validators import validate_email
-from django.core.exceptions import ValidationError
from api.models import Account, Invitation
from django.http import JsonResponse, HttpResponse, HttpResponseServerError, HttpResponseRedirect, HttpResponseBadRequest
-from django.shortcuts import redirect
from django.contrib.auth import authenticate, logout, login
from utils.custom_decorators import require_post_params
-from reserved_usernames import RESERVED_USERNAMES
from forms import AccountRegistrationForm, PasswordResetForm, RecoverUserForm
from api.tasks import send_email
from django.contrib.sites.shortcuts import get_current_site
+from django.shortcuts import get_object_or_404
from django.conf import settings
-from django.conf.urls import url
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
-from django.utils.http import base36_to_int, int_to_base36
+from django.utils.http import int_to_base36
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils.crypto import salted_hmac
@@ -26,6 +21,7 @@
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
""" Token Generator for Email Confirmation """
key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator"
+
def _make_token_with_timestamp(self, user, timestamp):
""" Token function pulled from Django 1.11 """
ts_b36 = int_to_base36(timestamp)
@@ -36,6 +32,7 @@ def _make_token_with_timestamp(self, user, timestamp):
).hexdigest()[::2]
return "%s-%s" % (ts_b36, hash)
+
account_activation_token = AccountActivationTokenGenerator()
@@ -57,6 +54,11 @@ def cw_login(request):
login(request, user)
if user.is_active:
+
+ account = get_object_or_404(Account, user=user)
+ request.session["login_user_firstname"] = account.first_name
+ request.session["login_user_image"] = account.profile_image_thumb_url
+
return HttpResponse()
else:
response = {
@@ -92,6 +94,8 @@ def cw_register(request):
user = authenticate(username=username, password=password)
account = Account(user=user)
+ if not settings.CLOSED_BETA:
+ account.beta_access = True
account.save()
user.is_active = True
diff --git a/authentication/forms.py b/project/authentication/forms.py
similarity index 94%
rename from authentication/forms.py
rename to project/authentication/forms.py
index efe952c93..9dba10b14 100644
--- a/authentication/forms.py
+++ b/project/authentication/forms.py
@@ -3,13 +3,12 @@
from django.contrib.auth.forms import (
UserCreationForm, SetPasswordForm, PasswordResetForm as AuthRecoverUserForm
)
-from django.utils.translation import ugettext, ugettext_lazy as _
-from api.models import Account
+from django.utils.translation import ugettext_lazy as _
from reserved_usernames import RESERVED_USERNAMES
from api.tasks import send_email as task_send_email
from django.contrib.sites.shortcuts import get_current_site
-from django.utils.encoding import force_bytes, force_text
-from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
import re
@@ -118,7 +117,7 @@ def save(self, domain_override=None,
domain = get_current_site(request).domain
base_url = "http://{domain}/auth/password_reset/{uid}/{token}/"
url_with_code = base_url.format(domain=domain, uid=uid, token=token)
- email_body = "You're receiving this email because you requested an account recovery email for your user account at {domain}. Your username for this email is: {username}. If you also need to reset your password, please go to the following page and choose a new password.".format(domain = domain, username = user.username)
+ email_body = "You're receiving this email because you requested an account recovery email for your user account at {domain}. Your username for this email is: {username}. If you also need to reset your password, please go to the following page and choose a new password.".format(domain=domain, username=user.username)
email_context = {
'title' : 'Account Recovery for CiviWiki',
diff --git a/authentication/reserved_usernames.py b/project/authentication/reserved_usernames.py
similarity index 100%
rename from authentication/reserved_usernames.py
rename to project/authentication/reserved_usernames.py
diff --git a/authentication/urls.py b/project/authentication/urls.py
similarity index 92%
rename from authentication/urls.py
rename to project/authentication/urls.py
index d39a1ec70..c4b508c01 100644
--- a/authentication/urls.py
+++ b/project/authentication/urls.py
@@ -1,6 +1,6 @@
from django.conf.urls import url
from django.contrib.auth.views import (
- password_reset, password_reset_done, password_reset_confirm, password_reset_complete
+ password_reset, password_reset_confirm
)
import authentication
diff --git a/civiwiki/__init__.py b/project/civiwiki/__init__.py
similarity index 100%
rename from civiwiki/__init__.py
rename to project/civiwiki/__init__.py
diff --git a/civiwiki/asgi.py b/project/civiwiki/asgi.py
similarity index 100%
rename from civiwiki/asgi.py
rename to project/civiwiki/asgi.py
diff --git a/civiwiki/celery.py b/project/civiwiki/celery.py
similarity index 100%
rename from civiwiki/celery.py
rename to project/civiwiki/celery.py
diff --git a/civiwiki/routing.py b/project/civiwiki/routing.py
similarity index 100%
rename from civiwiki/routing.py
rename to project/civiwiki/routing.py
diff --git a/civiwiki/settings.py b/project/civiwiki/settings.py
similarity index 95%
rename from civiwiki/settings.py
rename to project/civiwiki/settings.py
index 3ce3f2833..6b1f54624 100644
--- a/civiwiki/settings.py
+++ b/project/civiwiki/settings.py
@@ -57,7 +57,7 @@ def get_env_variable(environment_variable, optional=False):
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
- # 'django.middleware.csrf.CsrfViewMiddleware', TODO: fix eventually
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
@@ -66,6 +66,8 @@ def get_env_variable(environment_variable, optional=False):
)
+CSRF_USE_SESSIONS = True # Store the CSRF token in the users session instead of in a cookie
+
CORS_ORIGIN_ALLOW_ALL = True
ROOT_URLCONF = 'civiwiki.urls'
LOGIN_URL = '/login'
@@ -107,6 +109,11 @@ def get_env_variable(environment_variable, optional=False):
WSGI_APPLICATION = 'civiwiki.wsgi.application'
+# Global user privilege settings
+CLOSED_BETA = False
+if 'CLOSED_BETA' in os.environ:
+ CLOSED_BETA = get_env_variable("CLOSED_BETA")
+
# Apex Contact for Production Errors
ADMINS = [('Development Team', 'dev@civiwiki.org')]
diff --git a/civiwiki/urls.py b/project/civiwiki/urls.py
similarity index 100%
rename from civiwiki/urls.py
rename to project/civiwiki/urls.py
diff --git a/civiwiki/wsgi.py b/project/civiwiki/wsgi.py
similarity index 99%
rename from civiwiki/wsgi.py
rename to project/civiwiki/wsgi.py
index 516eaa27c..0067bf790 100644
--- a/civiwiki/wsgi.py
+++ b/project/civiwiki/wsgi.py
@@ -6,13 +6,10 @@
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
"""
-
-import os
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "civiwiki.settings")
-
from django.core.wsgi import get_wsgi_application
from whitenoise.django import DjangoWhiteNoise
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "civiwiki.settings")
application = get_wsgi_application()
application = DjangoWhiteNoise(application)
diff --git a/data/categories.json b/project/data/categories.json
similarity index 100%
rename from data/categories.json
rename to project/data/categories.json
diff --git a/data/congress.json b/project/data/congress.json
similarity index 100%
rename from data/congress.json
rename to project/data/congress.json
diff --git a/data/sample_threads.json b/project/data/sample_threads.json
similarity index 100%
rename from data/sample_threads.json
rename to project/data/sample_threads.json
diff --git a/frontend_views/__init__.py b/project/frontend_views/__init__.py
similarity index 100%
rename from frontend_views/__init__.py
rename to project/frontend_views/__init__.py
diff --git a/frontend_views/urls.py b/project/frontend_views/urls.py
similarity index 94%
rename from frontend_views/urls.py
rename to project/frontend_views/urls.py
index 79fa70365..85ea6f39b 100644
--- a/frontend_views/urls.py
+++ b/project/frontend_views/urls.py
@@ -13,6 +13,7 @@
url(r'^thread/(?P\w+)$', v.issue_thread, name='issue thread'),
url(r'^setup$', v.user_setup, name='user setup'),
url(r'^profile$', v.user_profile, name='default_profile'),
+ url(r'^settings$', v.settings_view, name='settings'),
url(r'^add_civi$', v.add_civi, name='add civi'),
url(r'^invite$', v.invite, name='invite'),
url(r'^beta_register/(?P[\w.%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})/(?P\w{31})$', v.beta_register, name='beta_register'),
diff --git a/frontend_views/views.py b/project/frontend_views/views.py
similarity index 90%
rename from frontend_views/views.py
rename to project/frontend_views/views.py
index 31d15fb2b..3821f0346 100644
--- a/frontend_views/views.py
+++ b/project/frontend_views/views.py
@@ -28,9 +28,9 @@ def base_view(request):
all_categories = list(Category.objects.values_list('id', flat=True))
user_categories = list(a.categories.values_list('id', flat=True)) or all_categories
- feed_threads = [Thread.objects.summarize(t) for t in Thread.objects.order_by('-created')]
- top5_threads = list(Thread.objects.all().order_by('-num_views')[:5].values('id', 'title'))
-
+ feed_threads = [Thread.objects.summarize(t) for t in Thread.objects.exclude(is_draft=True).order_by('-created')]
+ top5_threads = list(Thread.objects.filter(is_draft=False).order_by('-num_views')[:5].values('id', 'title'))
+ my_draft_threads = [Thread.objects.summarize(t) for t in Thread.objects.filter(author_id=a.id).exclude(is_draft=False).order_by('-created')]
states = sorted(US_STATES, key=lambda s: s[1])
data = {
@@ -38,7 +38,8 @@ def base_view(request):
'states': states,
'user_categories': user_categories,
'threads': feed_threads,
- 'trending': top5_threads
+ 'trending': top5_threads,
+ 'draft_threads': my_draft_threads
}
return TemplateResponse(request, 'feed.html', {'data': json.dumps(data)})
@@ -92,19 +93,9 @@ def issue_thread(request, thread_id=None):
req_acct = Account.objects.get(user=request.user)
t = Thread.objects.get(id=thread_id)
c_qs = Civi.objects.filter(thread_id=thread_id).exclude(c_type='response')
- # c_scored = Civi.objects.thread_sorted_by_score(c_qs, req_acct.id)
c_scored = [c.dict_with_score(req_acct.id) for c in c_qs]
civis = sorted(c_scored, key=lambda c: c['score'], reverse=True)
-
- # civis = Civi.objects.filter(thread_id=thread_id)
- # c = civis.order_by('-created')
- # c_scores = [ci.score(req_a.id) for ci in c]
- # c_data = [Civi.objects.serialize_s(ci) for ci in c]
- # for idx, item in enumerate(c_data):
- # c_data[idx]['score'] = c_scores[idx]
- # c_data = sorted(c_data, key=lambda x: x['score'], reverse=True)
-
#modify thread view count
t.num_civis = len(civis)
t.num_views = F('num_views') + 1
@@ -142,6 +133,7 @@ def issue_thread(request, thread_id=None):
data = {
'thread_id': thread_id,
+ 'is_draft': t.is_draft,
'thread_wiki_data': json.dumps(thread_wiki_data),
'thread_body_data': json.dumps(thread_body_data)
}
@@ -187,6 +179,23 @@ def invite(request):
return TemplateResponse(request, 'invite.html', response_data)
+@login_required
+@beta_blocker
+def settings_view(request):
+
+ request_account = Account.objects.get(user=request.user)
+
+ response_data = {
+ 'username': request.user.username,
+ 'email': request.user.email,
+ 'google_map_api_key': settings.GOOGLE_API_KEY,
+ 'sunlight_api_key': settings.SUNLIGHT_API_KEY,
+ 'lng': request_account.longitude,
+ 'lat': request_account.latitude
+ }
+
+ return TemplateResponse(request, 'user/settings.html', response_data)
+
def login_view(request):
if request.user.is_authenticated():
diff --git a/legislation/__init__.py b/project/legislation/__init__.py
similarity index 100%
rename from legislation/__init__.py
rename to project/legislation/__init__.py
diff --git a/legislation/migrations/__init__.py b/project/legislation/migrations/__init__.py
similarity index 100%
rename from legislation/migrations/__init__.py
rename to project/legislation/migrations/__init__.py
diff --git a/legislation/sunlightapi.py b/project/legislation/sunlightapi.py
similarity index 82%
rename from legislation/sunlightapi.py
rename to project/legislation/sunlightapi.py
index da6a43384..b6c095010 100644
--- a/legislation/sunlightapi.py
+++ b/project/legislation/sunlightapi.py
@@ -1,7 +1,7 @@
-import os
-import json
import sunlight
+from sunlight.errors import BadRequestException
from django.conf import settings
+from sunlight.errors import BadRequestException
def get_legislator_and_district(account):
location = account.zip_code
@@ -27,12 +27,12 @@ def get_bill_information(account):
def get_legislator_ids_by_lat_long(latitude, longitude):
""" Gets legislator bioguide_ids by coordinate location"""
- # sunlight.config.API_KEY = settings.SUNLIGHT_API_KEY
bioguide_ids = []
- # preferences = {}
- # preferences['state'] = account.state
- legislators = sunlight.congress.locate_legislators_by_lat_lon(latitude, longitude)
+ try:
+ legislators = sunlight.congress.locate_legislators_by_lat_lon(latitude, longitude)
+ except BadRequestException:
+ return bioguide_ids
if legislators and len(legislators):
for legislator in legislators:
diff --git a/manage.py b/project/manage.py
similarity index 100%
rename from manage.py
rename to project/manage.py
diff --git a/runtime.txt b/project/runtime.txt
similarity index 100%
rename from runtime.txt
rename to project/runtime.txt
diff --git a/setup.cfg b/project/setup.cfg
similarity index 81%
rename from setup.cfg
rename to project/setup.cfg
index 8a00af3c0..42641fd6c 100644
--- a/setup.cfg
+++ b/project/setup.cfg
@@ -1,3 +1,4 @@
[flake8]
ignore = E128,E265,E261,E126,E501,E302,E262,E127,E303,E226,E231,E201,E202,E121,E203,E123,W293,W391,E122,W292,F403,E401,E131,W503,E731,E266
max-line-length = 160
+exclude = api/migrations, __init__.py
diff --git a/utils/__init__.py b/project/utils/__init__.py
similarity index 100%
rename from utils/__init__.py
rename to project/utils/__init__.py
diff --git a/utils/constants.py b/project/utils/constants.py
similarity index 100%
rename from utils/constants.py
rename to project/utils/constants.py
diff --git a/utils/custom_decorators.py b/project/utils/custom_decorators.py
similarity index 94%
rename from utils/custom_decorators.py
rename to project/utils/custom_decorators.py
index 5ee964a68..49c0a037a 100644
--- a/utils/custom_decorators.py
+++ b/project/utils/custom_decorators.py
@@ -1,9 +1,6 @@
from functools import wraps
from django.utils.decorators import available_attrs
from django.http import HttpResponseBadRequest, HttpResponseRedirect
-from django.contrib.auth.views import redirect_to_login
-from django.shortcuts import resolve_url
-
from api.models import Account
'''
diff --git a/utils/string_templates.py b/project/utils/string_templates.py
similarity index 100%
rename from utils/string_templates.py
rename to project/utils/string_templates.py
diff --git a/webapp/static/dependencies/animate.min.css b/project/webapp/static/dependencies/animate.min.css
similarity index 100%
rename from webapp/static/dependencies/animate.min.css
rename to project/webapp/static/dependencies/animate.min.css
diff --git a/webapp/static/dependencies/backbone-min.js b/project/webapp/static/dependencies/backbone-min.js
similarity index 100%
rename from webapp/static/dependencies/backbone-min.js
rename to project/webapp/static/dependencies/backbone-min.js
diff --git a/webapp/static/dependencies/jquery.min.js b/project/webapp/static/dependencies/jquery.min.js
similarity index 100%
rename from webapp/static/dependencies/jquery.min.js
rename to project/webapp/static/dependencies/jquery.min.js
diff --git a/webapp/static/dependencies/less.min.js b/project/webapp/static/dependencies/less.min.js
similarity index 100%
rename from webapp/static/dependencies/less.min.js
rename to project/webapp/static/dependencies/less.min.js
diff --git a/webapp/static/dependencies/magicsuggest-min.css b/project/webapp/static/dependencies/magicsuggest-min.css
similarity index 100%
rename from webapp/static/dependencies/magicsuggest-min.css
rename to project/webapp/static/dependencies/magicsuggest-min.css
diff --git a/webapp/static/dependencies/magicsuggest-min.js b/project/webapp/static/dependencies/magicsuggest-min.js
similarity index 100%
rename from webapp/static/dependencies/magicsuggest-min.js
rename to project/webapp/static/dependencies/magicsuggest-min.js
diff --git a/webapp/static/dependencies/materialize.min.css b/project/webapp/static/dependencies/materialize.min.css
similarity index 100%
rename from webapp/static/dependencies/materialize.min.css
rename to project/webapp/static/dependencies/materialize.min.css
diff --git a/webapp/static/dependencies/materialize.min.js b/project/webapp/static/dependencies/materialize.min.js
similarity index 100%
rename from webapp/static/dependencies/materialize.min.js
rename to project/webapp/static/dependencies/materialize.min.js
diff --git a/webapp/static/dependencies/underscore.js b/project/webapp/static/dependencies/underscore.js
similarity index 100%
rename from webapp/static/dependencies/underscore.js
rename to project/webapp/static/dependencies/underscore.js
diff --git a/webapp/static/fonts/material-design-icons/LICENSE.txt b/project/webapp/static/fonts/material-design-icons/LICENSE.txt
similarity index 100%
rename from webapp/static/fonts/material-design-icons/LICENSE.txt
rename to project/webapp/static/fonts/material-design-icons/LICENSE.txt
diff --git a/webapp/static/fonts/material-design-icons/Material-Design-Icons.eot b/project/webapp/static/fonts/material-design-icons/Material-Design-Icons.eot
similarity index 100%
rename from webapp/static/fonts/material-design-icons/Material-Design-Icons.eot
rename to project/webapp/static/fonts/material-design-icons/Material-Design-Icons.eot
diff --git a/webapp/static/fonts/material-design-icons/Material-Design-Icons.svg b/project/webapp/static/fonts/material-design-icons/Material-Design-Icons.svg
similarity index 100%
rename from webapp/static/fonts/material-design-icons/Material-Design-Icons.svg
rename to project/webapp/static/fonts/material-design-icons/Material-Design-Icons.svg
diff --git a/webapp/static/fonts/material-design-icons/Material-Design-Icons.ttf b/project/webapp/static/fonts/material-design-icons/Material-Design-Icons.ttf
similarity index 100%
rename from webapp/static/fonts/material-design-icons/Material-Design-Icons.ttf
rename to project/webapp/static/fonts/material-design-icons/Material-Design-Icons.ttf
diff --git a/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff b/project/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff
similarity index 100%
rename from webapp/static/fonts/material-design-icons/Material-Design-Icons.woff
rename to project/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff
diff --git a/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff2 b/project/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff2
similarity index 100%
rename from webapp/static/fonts/material-design-icons/Material-Design-Icons.woff2
rename to project/webapp/static/fonts/material-design-icons/Material-Design-Icons.woff2
diff --git a/webapp/static/fonts/roboto/Roboto-Bold.eot b/project/webapp/static/fonts/roboto/Roboto-Bold.eot
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Bold.eot
rename to project/webapp/static/fonts/roboto/Roboto-Bold.eot
diff --git a/webapp/static/fonts/roboto/Roboto-Bold.ttf b/project/webapp/static/fonts/roboto/Roboto-Bold.ttf
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Bold.ttf
rename to project/webapp/static/fonts/roboto/Roboto-Bold.ttf
diff --git a/webapp/static/fonts/roboto/Roboto-Bold.woff b/project/webapp/static/fonts/roboto/Roboto-Bold.woff
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Bold.woff
rename to project/webapp/static/fonts/roboto/Roboto-Bold.woff
diff --git a/webapp/static/fonts/roboto/Roboto-Bold.woff2 b/project/webapp/static/fonts/roboto/Roboto-Bold.woff2
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Bold.woff2
rename to project/webapp/static/fonts/roboto/Roboto-Bold.woff2
diff --git a/webapp/static/fonts/roboto/Roboto-Light.eot b/project/webapp/static/fonts/roboto/Roboto-Light.eot
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Light.eot
rename to project/webapp/static/fonts/roboto/Roboto-Light.eot
diff --git a/webapp/static/fonts/roboto/Roboto-Light.ttf b/project/webapp/static/fonts/roboto/Roboto-Light.ttf
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Light.ttf
rename to project/webapp/static/fonts/roboto/Roboto-Light.ttf
diff --git a/webapp/static/fonts/roboto/Roboto-Light.woff b/project/webapp/static/fonts/roboto/Roboto-Light.woff
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Light.woff
rename to project/webapp/static/fonts/roboto/Roboto-Light.woff
diff --git a/webapp/static/fonts/roboto/Roboto-Light.woff2 b/project/webapp/static/fonts/roboto/Roboto-Light.woff2
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Light.woff2
rename to project/webapp/static/fonts/roboto/Roboto-Light.woff2
diff --git a/webapp/static/fonts/roboto/Roboto-Medium.eot b/project/webapp/static/fonts/roboto/Roboto-Medium.eot
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Medium.eot
rename to project/webapp/static/fonts/roboto/Roboto-Medium.eot
diff --git a/webapp/static/fonts/roboto/Roboto-Medium.ttf b/project/webapp/static/fonts/roboto/Roboto-Medium.ttf
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Medium.ttf
rename to project/webapp/static/fonts/roboto/Roboto-Medium.ttf
diff --git a/webapp/static/fonts/roboto/Roboto-Medium.woff b/project/webapp/static/fonts/roboto/Roboto-Medium.woff
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Medium.woff
rename to project/webapp/static/fonts/roboto/Roboto-Medium.woff
diff --git a/webapp/static/fonts/roboto/Roboto-Medium.woff2 b/project/webapp/static/fonts/roboto/Roboto-Medium.woff2
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Medium.woff2
rename to project/webapp/static/fonts/roboto/Roboto-Medium.woff2
diff --git a/webapp/static/fonts/roboto/Roboto-Regular.eot b/project/webapp/static/fonts/roboto/Roboto-Regular.eot
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Regular.eot
rename to project/webapp/static/fonts/roboto/Roboto-Regular.eot
diff --git a/webapp/static/fonts/roboto/Roboto-Regular.ttf b/project/webapp/static/fonts/roboto/Roboto-Regular.ttf
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Regular.ttf
rename to project/webapp/static/fonts/roboto/Roboto-Regular.ttf
diff --git a/webapp/static/fonts/roboto/Roboto-Regular.woff b/project/webapp/static/fonts/roboto/Roboto-Regular.woff
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Regular.woff
rename to project/webapp/static/fonts/roboto/Roboto-Regular.woff
diff --git a/webapp/static/fonts/roboto/Roboto-Regular.woff2 b/project/webapp/static/fonts/roboto/Roboto-Regular.woff2
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Regular.woff2
rename to project/webapp/static/fonts/roboto/Roboto-Regular.woff2
diff --git a/webapp/static/fonts/roboto/Roboto-Thin.eot b/project/webapp/static/fonts/roboto/Roboto-Thin.eot
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Thin.eot
rename to project/webapp/static/fonts/roboto/Roboto-Thin.eot
diff --git a/webapp/static/fonts/roboto/Roboto-Thin.ttf b/project/webapp/static/fonts/roboto/Roboto-Thin.ttf
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Thin.ttf
rename to project/webapp/static/fonts/roboto/Roboto-Thin.ttf
diff --git a/webapp/static/fonts/roboto/Roboto-Thin.woff b/project/webapp/static/fonts/roboto/Roboto-Thin.woff
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Thin.woff
rename to project/webapp/static/fonts/roboto/Roboto-Thin.woff
diff --git a/webapp/static/fonts/roboto/Roboto-Thin.woff2 b/project/webapp/static/fonts/roboto/Roboto-Thin.woff2
similarity index 100%
rename from webapp/static/fonts/roboto/Roboto-Thin.woff2
rename to project/webapp/static/fonts/roboto/Roboto-Thin.woff2
diff --git a/webapp/static/img/bg-landing-lastcall.jpg b/project/webapp/static/img/bg-landing-lastcall.jpg
similarity index 100%
rename from webapp/static/img/bg-landing-lastcall.jpg
rename to project/webapp/static/img/bg-landing-lastcall.jpg
diff --git a/webapp/static/img/bg-landing-tree.svg b/project/webapp/static/img/bg-landing-tree.svg
similarity index 100%
rename from webapp/static/img/bg-landing-tree.svg
rename to project/webapp/static/img/bg-landing-tree.svg
diff --git a/webapp/static/img/bg-supportus-main.png b/project/webapp/static/img/bg-supportus-main.png
similarity index 100%
rename from webapp/static/img/bg-supportus-main.png
rename to project/webapp/static/img/bg-supportus-main.png
diff --git a/webapp/static/img/bg-tile-cube.svg b/project/webapp/static/img/bg-tile-cube.svg
similarity index 100%
rename from webapp/static/img/bg-tile-cube.svg
rename to project/webapp/static/img/bg-tile-cube.svg
diff --git a/webapp/static/img/bg-tile-steps.png b/project/webapp/static/img/bg-tile-steps.png
similarity index 100%
rename from webapp/static/img/bg-tile-steps.png
rename to project/webapp/static/img/bg-tile-steps.png
diff --git a/webapp/static/img/content-civiwiki-infographic-l.png b/project/webapp/static/img/content-civiwiki-infographic-l.png
similarity index 100%
rename from webapp/static/img/content-civiwiki-infographic-l.png
rename to project/webapp/static/img/content-civiwiki-infographic-l.png
diff --git a/webapp/static/img/content-civiwiki-infographic-md.png b/project/webapp/static/img/content-civiwiki-infographic-md.png
similarity index 100%
rename from webapp/static/img/content-civiwiki-infographic-md.png
rename to project/webapp/static/img/content-civiwiki-infographic-md.png
diff --git a/webapp/static/img/favicon.ico b/project/webapp/static/img/favicon.ico
similarity index 100%
rename from webapp/static/img/favicon.ico
rename to project/webapp/static/img/favicon.ico
diff --git a/webapp/static/img/image-beta-indevelopment.gif b/project/webapp/static/img/image-beta-indevelopment.gif
similarity index 100%
rename from webapp/static/img/image-beta-indevelopment.gif
rename to project/webapp/static/img/image-beta-indevelopment.gif
diff --git a/webapp/static/img/logo-civiwiki-tree-black.svg b/project/webapp/static/img/logo-civiwiki-tree-black.svg
similarity index 100%
rename from webapp/static/img/logo-civiwiki-tree-black.svg
rename to project/webapp/static/img/logo-civiwiki-tree-black.svg
diff --git a/webapp/static/img/logo-civiwiki-tree-white.svg b/project/webapp/static/img/logo-civiwiki-tree-white.svg
similarity index 100%
rename from webapp/static/img/logo-civiwiki-tree-white.svg
rename to project/webapp/static/img/logo-civiwiki-tree-white.svg
diff --git a/webapp/static/img/logo-social-facebook.png b/project/webapp/static/img/logo-social-facebook.png
similarity index 100%
rename from webapp/static/img/logo-social-facebook.png
rename to project/webapp/static/img/logo-social-facebook.png
diff --git a/webapp/static/img/logo-social-twitter.png b/project/webapp/static/img/logo-social-twitter.png
similarity index 100%
rename from webapp/static/img/logo-social-twitter.png
rename to project/webapp/static/img/logo-social-twitter.png
diff --git a/webapp/static/img/logo-sponsor-gephardt.png b/project/webapp/static/img/logo-sponsor-gephardt.png
similarity index 100%
rename from webapp/static/img/logo-sponsor-gephardt.png
rename to project/webapp/static/img/logo-sponsor-gephardt.png
diff --git a/webapp/static/img/logo-sponsor-gusf.png b/project/webapp/static/img/logo-sponsor-gusf.png
similarity index 100%
rename from webapp/static/img/logo-sponsor-gusf.png
rename to project/webapp/static/img/logo-sponsor-gusf.png
diff --git a/webapp/static/img/logo-sponsor-skandalaris.png b/project/webapp/static/img/logo-sponsor-skandalaris.png
similarity index 100%
rename from webapp/static/img/logo-sponsor-skandalaris.png
rename to project/webapp/static/img/logo-sponsor-skandalaris.png
diff --git a/webapp/static/img/logo-wordmark.png b/project/webapp/static/img/logo-wordmark.png
similarity index 100%
rename from webapp/static/img/logo-wordmark.png
rename to project/webapp/static/img/logo-wordmark.png
diff --git a/webapp/static/img/no_image_md.png b/project/webapp/static/img/no_image_md.png
similarity index 100%
rename from webapp/static/img/no_image_md.png
rename to project/webapp/static/img/no_image_md.png
diff --git a/webapp/static/img/team/dan.png b/project/webapp/static/img/team/dan.png
similarity index 100%
rename from webapp/static/img/team/dan.png
rename to project/webapp/static/img/team/dan.png
diff --git a/webapp/static/img/team/darius.png b/project/webapp/static/img/team/darius.png
similarity index 100%
rename from webapp/static/img/team/darius.png
rename to project/webapp/static/img/team/darius.png
diff --git a/webapp/static/img/team/joohee.png b/project/webapp/static/img/team/joohee.png
similarity index 100%
rename from webapp/static/img/team/joohee.png
rename to project/webapp/static/img/team/joohee.png
diff --git a/webapp/static/img/team/mitchell.png b/project/webapp/static/img/team/mitchell.png
similarity index 100%
rename from webapp/static/img/team/mitchell.png
rename to project/webapp/static/img/team/mitchell.png
diff --git a/webapp/static/js/account.js b/project/webapp/static/js/account.js
similarity index 93%
rename from webapp/static/js/account.js
rename to project/webapp/static/js/account.js
index 3fff5e715..a9879d159 100644
--- a/webapp/static/js/account.js
+++ b/project/webapp/static/js/account.js
@@ -52,7 +52,7 @@ cw.AccountView = BB.View.extend({
initialize: function (options) {
options = options || {};
- this.mapView = options.mapView;
+ // this.mapView = options.mapView;
this.current_user = options.current_user;
this.isSave = false;
@@ -79,16 +79,10 @@ cw.AccountView = BB.View.extend({
this.postRender();
} else {
this.$el.empty().append(this.template());
+
+ $('.account-tabs .tab').on('dragstart', function() {return false;});
this.$el.find('.account-settings').pushpin({ top: $('.account-settings').offset().top });
this.$el.find('.scroll-col').height($(window).height());
-
- // Only render the settings form if logged in user
- var settingsEl = this.$('#settings');
- if (settingsEl.length) {
- settingsEl.empty().append(this.settingsTemplate());
- this.mapView.renderAndInitMap();
- this.listenTo(this.mapView.model, 'change:address', _.bind(this.saveLocation, this)); //TODO: validateLocation and then save
- }
}
},
@@ -102,7 +96,7 @@ cw.AccountView = BB.View.extend({
var settingsEl = this.$('#settings');
if (settingsEl.length) {
settingsEl.empty().append(this.settingsTemplate());
- this.mapView.render();
+ Materialize.updateTextFields();
}
},
@@ -119,11 +113,11 @@ cw.AccountView = BB.View.extend({
events: {
'click .follow-btn': 'followRequest',
'submit #profile_image_form': 'handleFiles',
- 'change .profile-image-pick': 'toggleImgButtons',
'blur .save-account': 'saveAccount',
'mouseenter .user-chip-contents': 'showUserCard',
'mouseleave .user-chip-contents': 'hideUserCard',
'click .toggle-solutions': 'toggleSolutions',
+ 'change .profile-image-pick': 'previewImage',
'keypress .save-account': cw.checkForEnter,
},
@@ -135,6 +129,27 @@ cw.AccountView = BB.View.extend({
this.$('#solutions-'+id).toggleClass('hide');
},
+ previewImage: function(e){
+ var _this = this;
+ var img = this.$el.find('#id_profile_image');
+ if (img.val()) {
+ var uploaded_image = img[0].files[0];
+ if (uploaded_image) {
+ var formData = new FormData(this.$el.find('#profile_image_form')[0]);
+
+ var reader = new FileReader();
+
+ reader.onload = function(e) {
+ var preview_image = _this.$el.find('.preview-image');
+ preview_image.attr('src', e.target.result);
+
+ _this.toggleImgButtons();
+ };
+ reader.readAsDataURL(uploaded_image);
+ }
+ }
+ },
+
showUserCard: function(e) {
var _this = this;
var username = e.currentTarget.dataset.username;
@@ -221,6 +236,7 @@ cw.AccountView = BB.View.extend({
toggleImgButtons: function(event) {
this.$('.profile-image-pick').toggleClass('hide');
this.$('.upload-image').toggleClass('hide');
+ this.$('#confirmation-prompt').toggleClass('hide');
},
followRequest: function(e){
@@ -269,7 +285,7 @@ cw.AccountView = BB.View.extend({
saveAccount: function (e) {
var $this = $(e.target),
changeKey = $this.attr('id'),
- changeVal = $this.val(),
+ changeVal = $this.val().trim(),
apiData = {},
_this = this;
diff --git a/webapp/static/js/account_tabs/followers.js b/project/webapp/static/js/account_tabs/followers.js
similarity index 100%
rename from webapp/static/js/account_tabs/followers.js
rename to project/webapp/static/js/account_tabs/followers.js
diff --git a/webapp/static/js/account_tabs/following.js b/project/webapp/static/js/account_tabs/following.js
similarity index 100%
rename from webapp/static/js/account_tabs/following.js
rename to project/webapp/static/js/account_tabs/following.js
diff --git a/webapp/static/js/account_tabs/my_civis.js b/project/webapp/static/js/account_tabs/my_civis.js
similarity index 100%
rename from webapp/static/js/account_tabs/my_civis.js
rename to project/webapp/static/js/account_tabs/my_civis.js
diff --git a/webapp/static/js/account_tabs/my_issues.js b/project/webapp/static/js/account_tabs/my_issues.js
similarity index 100%
rename from webapp/static/js/account_tabs/my_issues.js
rename to project/webapp/static/js/account_tabs/my_issues.js
diff --git a/webapp/static/js/account_tabs/my_reps.js b/project/webapp/static/js/account_tabs/my_reps.js
similarity index 100%
rename from webapp/static/js/account_tabs/my_reps.js
rename to project/webapp/static/js/account_tabs/my_reps.js
diff --git a/project/webapp/static/js/base.js b/project/webapp/static/js/base.js
new file mode 100644
index 000000000..972d5b9cb
--- /dev/null
+++ b/project/webapp/static/js/base.js
@@ -0,0 +1,68 @@
+cw = {};
+BB = Backbone;
+
+_.templateSettings = {
+ evaluate: /\{\{#(.+?)\}\}/g,
+ interpolate: /\{\{=(.+?)\}\}/g,
+ escape: /\{\{(?!#|=)(.+?)\}\}/g
+};
+
+cw.underscorePartial = function (templateSelector, data) {
+ return _.template($('#' + templateSelector).html())(data);
+};
+
+cw.checkForEnter = function (e) {
+ if (e.which == 13 && !e.shiftKey) {
+ e.preventDefault();
+ $(e.target).blur();
+ }
+};
+
+cw.materializeShit = function () {
+ Materialize.updateTextFields();
+ $('ul.tabs').tabs();
+ $('select').material_select();
+};
+
+cw.initGlobalNav = function () {
+ var $notifications = $('#item-notifications');
+
+ $notifications.on('click', function () {
+ $('.notifications-modal').openModal();
+ });
+
+ var $navMenuButton = $('#js-toggle-menu');
+ $navMenuButton.on('click', function () {
+ event.stopPropagation()
+ $('#js-dropdown-menu').toggleClass('hide');
+ $('.js-dropdown-icon').toggleClass('hide');
+
+ $(document).on('click', function(event){
+ $target = $(event.target);
+
+ if ($target.parents('#js-dropdown-menu').length==0){
+ $('#js-dropdown-menu').addClass('hide');
+ $('.js-dropdown-icon').toggleClass('hide');
+ $(this).unbind(event);
+ }
+ })
+
+
+ });
+
+};
+
+//CSRF Setup
+cw.csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
+
+function csrfSafeMethod(method) {
+ // these HTTP methods do not require CSRF protection
+ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+}
+$.ajaxSetup({
+ beforeSend: function(xhr, settings) {
+ if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+ xhr.setRequestHeader("X-CSRFToken", cw.csrftoken);
+ }
+ }
+});
diff --git a/webapp/static/js/beta_register.js b/project/webapp/static/js/beta_register.js
similarity index 100%
rename from webapp/static/js/beta_register.js
rename to project/webapp/static/js/beta_register.js
diff --git a/webapp/static/js/beta_view.js b/project/webapp/static/js/beta_view.js
similarity index 100%
rename from webapp/static/js/beta_view.js
rename to project/webapp/static/js/beta_view.js
diff --git a/webapp/static/js/feed.js b/project/webapp/static/js/feed.js
similarity index 76%
rename from webapp/static/js/feed.js
rename to project/webapp/static/js/feed.js
index be76c3097..a5cd5beb7 100644
--- a/webapp/static/js/feed.js
+++ b/project/webapp/static/js/feed.js
@@ -48,6 +48,7 @@ cw.FeedView = BB.View.extend({
this.options = options || {};
this.categories = this.options.categories;
this.threads = this.options.threads.toJSON();
+ this.draft_threads = this.options.draft_threads.toJSON();
this.render();
this.options.threads.on('sync', function() {
@@ -62,6 +63,7 @@ cw.FeedView = BB.View.extend({
var template_var = {
'trending': this.options.trending,
+ 'draft_threads': this.draft_threads,
'categories': this.options.categories,
'user_categories': filtered_categories,
};
@@ -190,6 +192,7 @@ cw.NewThreadView = BB.View.extend({
events: {
'click .cancel-new-thread': 'cancelThread',
'click .create-new-thread': 'createThread',
+ 'click .draft-new-thread': 'draftThread',
'click #image-from-computer': 'showImageUploadForm',
'click #image-from-link': 'showImageLinkForm',
'change #thread-location': 'showStates',
@@ -295,6 +298,86 @@ cw.NewThreadView = BB.View.extend({
Materialize.toast('Please input all fields.', 5000);
}
},
+
+ draftThread: function () {
+ var _this = this;
+
+ var title = this.$el.find('#thread-title').val(),
+ summary = this.$el.find('#thread-body').val(),
+ level = this.$el.find('#thread-location').val(),
+ category_id = this.$el.find('#thread-category').val(),
+ state="";
+ if (level === "state" ) {
+ state = this.$el.find('#thread-state').val();
+ }
+ if (title && summary && category_id) {
+ $.ajax({
+ url: '/api/new_thread/',
+ type: 'POST',
+ data: {
+ title: title,
+ summary: summary,
+ category_id: category_id,
+ level: level,
+ state: state,
+ is_draft: "True"
+ },
+ success: function (response) {
+ if (_this.imageMode==="upload") {
+ var file = $('#id_attachment_image').val();
+
+ if (file) {
+ var formData = new FormData(_this.$('#attachment_image_form')[0]);
+ formData.set('thread_id', response.thread_id);
+ $.ajax({
+ url: '/api/upload_image/',
+ type: 'POST',
+ success: function () {
+ Materialize.toast('New thread created.', 5000);
+ window.location = "thread/" + response.thread_id;
+ },
+ error: function(e){
+ Materialize.toast('ERROR: Image could not be uploaded', 5000);
+ Materialize.toast(e.statusText, 5000);
+ },
+ data: formData,
+ cache: false,
+ contentType: false,
+ processData: false
+ });
+ }
+ } else if (_this.imageMode==="link") {
+ var img_url = _this.$('#link-image-form').val().trim();
+ console.log(_this.imageMode, img_url);
+ if (img_url) {
+ $.ajax({
+ url: '/api/upload_image/',
+ type: 'POST',
+ data: {
+ link: img_url,
+ thread_id: response.thread_id
+ },
+ success: function () {
+ Materialize.toast('New thread created.', 5000);
+ window.location = "thread/" + response.thread_id;
+ },
+ error: function(e){
+ Materialize.toast('ERROR: Image could not be uploaded', 5000);
+ Materialize.toast(e.statusText, 5000);
+ }
+ });
+ }
+ } else {
+ Materialize.toast('New thread created.', 5000);
+ window.location = "thread/" + response.thread_id;
+ }
+
+ }
+ });
+ } else {
+ Materialize.toast('Please input all fields.', 5000);
+ }
+ },
});
diff --git a/webapp/static/js/general_view.js b/project/webapp/static/js/general_view.js
similarity index 100%
rename from webapp/static/js/general_view.js
rename to project/webapp/static/js/general_view.js
diff --git a/webapp/static/js/invite.js b/project/webapp/static/js/invite.js
similarity index 100%
rename from webapp/static/js/invite.js
rename to project/webapp/static/js/invite.js
diff --git a/webapp/static/js/login.js b/project/webapp/static/js/login.js
similarity index 100%
rename from webapp/static/js/login.js
rename to project/webapp/static/js/login.js
diff --git a/webapp/static/js/notifications.js b/project/webapp/static/js/notifications.js
similarity index 93%
rename from webapp/static/js/notifications.js
rename to project/webapp/static/js/notifications.js
index 81a4bbdf0..19dc57953 100644
--- a/webapp/static/js/notifications.js
+++ b/project/webapp/static/js/notifications.js
@@ -45,6 +45,7 @@ $('#item-notifications').on('click', function () {
url: '/inbox/notifications/mark-all-as-read/',
success: function (response) {
$('#notify-count-wrapper').addClass('hide');
+ $('#notify-icon').html('notifications_none').removeClass('active');
console.log('success');
},
error: function(r){
@@ -57,8 +58,11 @@ var render_notifications = function(data) {
var unreadCount = parseInt(data.unread_count);
if (unreadCount === 0) {
$('#notify-count-wrapper').addClass('hide');
+ $('#notify-icon').html('notifications_none').removeClass('active');
} else {
$('#notify-count-wrapper').removeClass('hide');
+ $('#notify-icon').html('notifications').addClass('active');
+
this.new_notifications = [];
var notified = false;
if (data.unread_list.length > 5 && !notificationsView.initialNotificationShown) {
diff --git a/project/webapp/static/js/settings.js b/project/webapp/static/js/settings.js
new file mode 100644
index 000000000..b30e17f69
--- /dev/null
+++ b/project/webapp/static/js/settings.js
@@ -0,0 +1,135 @@
+cw = cw || {};
+
+cw.UserModel = BB.Model.extend({
+ defaults: function() {
+ return {
+ username: "",
+ email: "",
+ location: "",
+ };
+ },
+ url: function () {
+ if (! this.get('username') ) {
+ throw new Error("This is a race condition! and why we can't have nice things :(");
+ }
+ return '/api/account_profile/' + this.get('username') + '/';
+ },
+
+ initialize: function (model, options) {
+ options = options || {};
+ }
+});
+
+
+cw.SettingsView = BB.View.extend({
+
+ el: '#settings',
+
+ initialize: function(options) {
+ this.options = options || {};
+ this.mapView = options.mapView;
+
+ this.template = _.template($('#settings-template').text());
+ this.settingsTemplate = _.template($('#settings-base').text());
+ this.personalTemplate = _.template($('#settings-personal').text());
+ this.locationLabelTemplate = _.template($('#location-label').text());
+
+ this.listenTo(this.model, 'change', this.renderAllLabels);
+ },
+
+ render: function() {
+ this.$el.html(this.template());
+
+ this.$('#settings-el').html(this.settingsTemplate());
+
+ this.renderPersonal();
+
+ this.mapView.renderAndInitMap();
+ // var saveLocationThrottled = _.throttle(this.saveLocation, 1000);
+ this.listenTo(this.mapView.model, 'change:is_new', _.bind(this.saveLocation, this));
+ },
+
+ renderAllLabels:function() {
+ this.renderPersonal();
+ this.renderLocationLabel();
+ Materialize.updateTextFields();
+ },
+
+ renderPersonal: function() {
+ this.$('#settings-1').html(this.personalTemplate());
+ },
+
+ renderLocationLabel: function() {
+ this.$('#location-label-container').html(this.locationLabelTemplate());
+ },
+
+ events: {
+ 'blur .save-account': 'saveAccount',
+ },
+
+ saveAccount: function (e) {
+ var $this = $(e.target),
+ changeKey = $this.attr('id'),
+ changeVal = $this.val().trim(),
+ apiData = {},
+ _this = this;
+
+ if (this.model.get([changeKey]) === changeVal) {
+ return;
+ }
+
+ apiData[changeKey] = changeVal;
+
+ $.ajax({
+ url: '/api/edituser/',
+ type: 'POST',
+ data: apiData,
+ success: function () {
+ Materialize.toast('Saved!', 5000);
+
+ _this.isSave = true;
+ _this.model.fetch();
+
+ }
+ });
+ },
+
+ saveLocation: function () {
+ var _this = this;
+ var coordinates = this.mapView.model.get('coordinates'),
+ address = this.mapView.model.get('address');
+
+ if (!this.mapView.model.get('is_new')) return;
+
+ if (coordinates && address) {
+
+ $.ajax({
+ type: 'POST',
+ url: '/api/edituser/',
+ data: {
+ coordinates: coordinates,
+ address: address.address,
+ city: address.city,
+ state: address.state,
+ zip_code: address.zipcode,
+ longitude: coordinates.lng,
+ latitude: coordinates.lat,
+ },
+ success: function (data) {
+ Materialize.toast('Location Changed', 5000);
+ _this.mapView.model.set('is_new', false);
+ _this.model.fetch();
+ },
+ error: function (data) {
+ if (data.status_code === 400) {
+ Materialize.toast(data.message, 5000);
+ } else if (data.status_code === 500) {
+ Materialize.toast('Internal Server Error', 5000);
+ } else {
+ Materialize.toast(data.statusText, 5000);
+ }
+ }
+ });
+ }
+ },
+});
diff --git a/webapp/static/js/static/about_view.js b/project/webapp/static/js/static/about_view.js
similarity index 100%
rename from webapp/static/js/static/about_view.js
rename to project/webapp/static/js/static/about_view.js
diff --git a/webapp/static/js/static/how_it_works_view.js b/project/webapp/static/js/static/how_it_works_view.js
similarity index 100%
rename from webapp/static/js/static/how_it_works_view.js
rename to project/webapp/static/js/static/how_it_works_view.js
diff --git a/webapp/static/js/static/landing_view.js b/project/webapp/static/js/static/landing_view.js
similarity index 100%
rename from webapp/static/js/static/landing_view.js
rename to project/webapp/static/js/static/landing_view.js
diff --git a/webapp/static/js/static/support_us_view.js b/project/webapp/static/js/static/support_us_view.js
similarity index 100%
rename from webapp/static/js/static/support_us_view.js
rename to project/webapp/static/js/static/support_us_view.js
diff --git a/webapp/static/js/thread.js b/project/webapp/static/js/thread.js
similarity index 92%
rename from webapp/static/js/thread.js
rename to project/webapp/static/js/thread.js
index 5d2f43330..8eb5acc1f 100644
--- a/webapp/static/js/thread.js
+++ b/project/webapp/static/js/thread.js
@@ -178,6 +178,7 @@ cw.CiviView = BB.View.extend({
initialize: function (options) {
this.options = options || {};
this.can_edit = options.can_edit;
+ this.is_draft = options.is_draft;
this.can_respond = options.can_respond;
this.parentView = options.parentView;
this.civis = this.parentView.civis;
@@ -391,15 +392,23 @@ cw.CiviView = BB.View.extend({
if (this.model.get('type') === 'response' || this.model.get('type') === 'rebuttal') {
this.$('.edit-links').addClass('hide');
this.$('#civi-type-form').addClass('hide');
+ } else if (this.model.get('type') === 'problem') {
+ this.$('#magicsuggest-'+this.model.id).addClass('hide');
}
},
clickNewType: function(e){
var new_type = $(e.target).closest("input[type='radio']:checked").val();
// var new_type = $("#civi-type-form input[type='radio']:checked").val();
+ if (new_type === "problem") {
+ this.$('.edit-links').addClass('hide');
+ this.$('#magicsuggest-'+this.model.id).addClass('hide');
+ } else {
+ this.$('.edit-links').removeClass('hide');
+ this.$('#magicsuggest-'+this.model.id).removeClass('hide');
+ }
this.magicSuggestView.setLinkableData(new_type);
this.magicSuggestView.ms.clear();
- Materialize.toast('Changing the civi type has cleared your links', 5000);
},
addRebuttal: function(){
@@ -668,10 +677,13 @@ cw.NewCiviView = BB.View.extend({
render: function () {
this.$el.empty().append(this.template());
- $('.responses').height($('#new-civi-box').height() + $('.responses-box').height());
+ // $('.responses').height($('#new-civi-box').height() + $('.responses-box').height());
this.magicSuggestView = new cw.LinkSelectView({$el: this.$('#magicsuggest'), civis: this.options.parentView.civis});
+ this.$('.edit-links').addClass('hide');
+ this.$('#magicsuggest').addClass('hide');
+
this.attachment_links = [];
this.attachmentCount = 0;
// this.renderMagicSuggest();
@@ -686,6 +698,7 @@ cw.NewCiviView = BB.View.extend({
'click #image-from-computer': 'showImageUploadForm',
'click #image-from-link': 'showImageLinkForm',
'click #add-image-link-input': 'addImageLinkInput',
+ 'click .ms-sel-ctn': ''
},
addImageLinkInput: function(){
@@ -735,7 +748,7 @@ cw.NewCiviView = BB.View.extend({
cancelCivi: function () {
this.$el.empty();
- $('.responses').height($('.responses-box').height());
+ // $('.responses').height($('.responses-box').height());
},
createCivi: function (e) {
@@ -875,7 +888,17 @@ cw.NewCiviView = BB.View.extend({
$this.siblings().removeClass('current');
var c_type = this.$el.find('.civi-types > .current').val();
- this.magicSuggestView.setLinkableData(c_type);
+
+ if (c_type === "problem") {
+ this.$('.edit-links').addClass('hide');
+ this.$('#magicsuggest').addClass('hide');
+ } else {
+ this.$('.edit-links').removeClass('hide');
+ this.$('#magicsuggest').removeClass('hide');
+ this.magicSuggestView.setLinkableData(c_type);
+ this.magicSuggestView.ms.clear();
+ }
+
},
});
@@ -1286,6 +1309,7 @@ cw.LinkSelectView = BB.View.extend({
groupBy: 'type',
valueField: 'id',
displayField: 'title',
+ expandOnFocus: true,
data: [],
renderer: function(data){
return ''+limit+'/'+totalCount+ ' '+ type+'s View More +
');
@@ -1700,8 +1701,9 @@ cw.ThreadView = BB.View.extend({
},
civiRenderHelper: function(civi){
+ var is_draft = this.is_draft;
var can_edit = civi.get('author').username == this.username ? true : false;
- this.$('#thread-'+civi.get('type')+'s').append(new cw.CiviView({model: civi, can_edit: can_edit, parentView: this}).el);
+ this.$('#thread-'+civi.get('type')+'s').append(new cw.CiviView({model: civi, can_edit: can_edit, is_draft: is_draft, parentView: this}).el);
},
@@ -1756,6 +1758,11 @@ cw.ThreadView = BB.View.extend({
}
}
}, this);
+
+ // add padding
+ var $lastResponseCivi = this.$('#response-list div:last-child');
+ var scrollPadding = this.$('.responses').height() - $lastResponseCivi.height();
+ this.$('.responses-padding').height(scrollPadding - 8);
},
events: {
@@ -1768,7 +1775,8 @@ cw.ThreadView = BB.View.extend({
'click .add-response': 'openNewResponseModal',
'click #recommended-switch': 'toggleRecommended',
'click .btn-loadmore': 'loadMoreCivis',
- 'click .edit-thread-button': 'openEditThreadModal'
+ 'click .edit-thread-button': 'openEditThreadModal',
+ 'click #js-publish-btn': 'publishThread'
},
scrollToBody: function () {
@@ -1778,38 +1786,29 @@ cw.ThreadView = BB.View.extend({
this.$('.thread-body-holder').removeClass('hide');
this.$('.thread-body-holder').css({display: 'block'});
+ this.resizeBodyToWindow();
+
+ this.renderOutline();
+ this.processCiviScroll();
+ },
+
+ resizeBodyToWindow: function() {
$('body').css({overflow: 'hidden'});
- // this.$('.thread-body-holder').css({display: 'block'});
- // $('body').css({overflow: 'hidden'});
- //
- // $('body').animate({
- // scrollTop: $('.thread-body-holder').offset().top
- // }, 200);
+ var $windowHeight = $('body').height();
+ var bodyHeight = $windowHeight - $("#js-global-nav").height();
var $civiNavScroll = this.$('.civi-outline');
- $civiNavScroll.css({height: $('body').height() - $civiNavScroll.offset().top});
+ $civiNavScroll.css({height: $windowHeight - $civiNavScroll.offset().top});
var $civiThreadScroll = this.$('.main-thread');
- $civiThreadScroll.css({height: $('body').height() - $civiThreadScroll.offset().top});
- var $civiResponseScroll = this.$('.responses-box');
- $civiResponseScroll.css({height: $('body').height() - $civiResponseScroll.offset().top});
-
- this.renderOutline();
-
- // this.currentScroll = 0;
- this.processCiviScroll();
+ $civiThreadScroll.css({height: $windowHeight - $civiThreadScroll.offset().top});
+ var $civiResponseScroll = this.$('.responses');
+ $civiResponseScroll.css({height: $windowHeight - $civiResponseScroll.offset().top});
},
-
scrollToWiki: function () {
var _this = this;
- // $('body').animate({
- // scrollTop: 0
- // }, 200, function () {
- // _this.$('.thread-body-holder').css({display: 'none'});
- // });
- // $('body').css({overflow: 'scroll'});
this.$('.thread-body-holder').addClass('hide');
this.$('.thread-wiki-holder').removeClass('hide');
$('body').css({overflow: 'scroll'});
@@ -1824,23 +1823,12 @@ cw.ThreadView = BB.View.extend({
var _this = this,
$this = _.isUndefined(e) ? this.$('.expand-nav') : $(e.target);
- // if ($this.hasClass('expanded')) {
- // $('.civi-nav-wrapper').slideUp();
- // $this.removeClass('expanded');
- // this.navExpanded = false;
- // } else {
- // $('.civi-nav-wrapper').slideDown();
- // $this.addClass('expanded');
- // this.navExpanded = true;
- // }
if (!this.navExpanded) {
$('.civi-nav-wrapper').hide();
$this.removeClass('expanded');
- // this.navExpanded = false;
} else {
$('.civi-nav-wrapper').show();
$this.addClass('expanded');
- // this.navExpanded = true;
}
this.activateNav();
},
@@ -1990,7 +1978,6 @@ cw.ThreadView = BB.View.extend({
drilldownCivi: function (e) {
var target = $(e.target);
- console.log(target);
var ms_check = target.hasClass('ms-ctn') || target.hasClass('ms-sel-ctn') || target.hasClass('ms-close-btn') || target.hasClass('ms-trigger') || target.hasClass('ms-trigger-ico') || target.hasClass('ms-res-ctn') || target.hasClass('ms-sel-item') || target.hasClass('ms-res-group');
if (target.hasClass('btn') || target.hasClass('rating-button') || target.is('input') || target.is('textarea') || target.is('label') || target.hasClass('input') || ms_check) {
@@ -2069,6 +2056,36 @@ cw.ThreadView = BB.View.extend({
this.selectInitialCiviAfterToggle();
},
+ publishThread: function(e){
+ var _this = this;
+ _this.$(e.currentTarget).addClass('disabled').attr('disabled', true);
+
+ $.ajax({
+ url: '/api/edit_thread/',
+ type: 'POST',
+ data: {
+ thread_id: _this.model.threadId,
+ is_draft: false,
+ },
+ success: function (response) {
+ _this.is_draft = false
+ Materialize.toast('Thread is now public. Refreshing the page...', 5000);
+ _this.$("#js-publish-btn").hide()
+ var reload_page = _.bind(location.reload, location);
+ _.delay(reload_page, 1000);
+ },
+ error: function (response) {
+ if (response.status === 403) {
+ Materialize.toast('You do not have permission to publish the thread', 5000);
+ }
+ else if (response.status === 500) {
+ Materialize.toast('Server Error: Thread could not be published', 5000);
+ _this.$(e.currentTarget).removeClass('disabled').attr('disabled', false);
+ }
+ }
+ });
+ },
+
openNewCiviModal: function () {
this.newCiviView.render();
},
diff --git a/webapp/static/js/user-setup.js b/project/webapp/static/js/user-setup.js
similarity index 99%
rename from webapp/static/js/user-setup.js
rename to project/webapp/static/js/user-setup.js
index 2f27df886..011d438fb 100644
--- a/webapp/static/js/user-setup.js
+++ b/project/webapp/static/js/user-setup.js
@@ -199,7 +199,7 @@ cw.UserSetupView = BB.View.extend({
type: 'POST',
url: '/api/edituser/',
data: {
- full_account: true,
+ full_account: 'True',
about_me: about_me,
first_name: first_name,
last_name: last_name,
diff --git a/webapp/static/js/utils/locate.js b/project/webapp/static/js/utils/locate.js
similarity index 97%
rename from webapp/static/js/utils/locate.js
rename to project/webapp/static/js/utils/locate.js
index 4663f2006..65299ef16 100644
--- a/webapp/static/js/utils/locate.js
+++ b/project/webapp/static/js/utils/locate.js
@@ -6,7 +6,7 @@ cw.Map = BB.Model.extend({
id: '',
uscapitol: { lat: 38.8899389, lng: -77.0090505 },
zoom: 5,
- coordinates: {}, address: {},
+ coordinates: {}, address: {}, is_new: false,
// Visual Setup and Initalization Options
mapOptions: {}, markerOptions: {},
// Google Map Objects
@@ -166,6 +166,12 @@ cw.MapView = BB.View.extend({
fillInAddress: function () {
this.$('.progress').toggleClass('hide');
var place = this.model.get('autocomplete').getPlace();
+ // Check if user didn't select any Options
+ if (_.isUndefined(place.address_components)) {
+ this.$('.progress').toggleClass('hide');
+ return;
+ }
+
this.model.set('address', this.getAddressFromComponents(place.address_components));
// Set the model's coordinates
var coordinates = {
@@ -173,7 +179,7 @@ cw.MapView = BB.View.extend({
lng: place.geometry.location.lng()
};
this.model.set('coordinates', coordinates);
- this.getLegislators(coordinates);
+ this.model.set('is_new', true);
this.adjustMapCenter(coordinates);
this.$('.progress').toggleClass('hide');
@@ -199,13 +205,13 @@ cw.MapView = BB.View.extend({
}
_this.model.set('coordinates', geolocation);
- _this.getLegislators(geolocation);
_this.adjustMapCenter(geolocation);
_this.model.get('geocoder').geocode({ 'location': geolocation }, function(results, status) {
if (status === 'OK') {
_this.$('#autocomplete').val(results[0].formatted_address);
_this.model.set('address', _this.getAddressFromComponents(results[0].address_components));
+ _this.model.set('is_new', true);
} else {
Materialize.toast('Geocode Error: ' + status, 2000);
}
diff --git a/webapp/static/less/about.less b/project/webapp/static/less/about.less
similarity index 100%
rename from webapp/static/less/about.less
rename to project/webapp/static/less/about.less
diff --git a/webapp/static/less/account.less b/project/webapp/static/less/account.less
similarity index 94%
rename from webapp/static/less/account.less
rename to project/webapp/static/less/account.less
index 1dbff3f7d..029a35930 100644
--- a/webapp/static/less/account.less
+++ b/project/webapp/static/less/account.less
@@ -184,25 +184,21 @@ body {
}
}
+ .preview-image {
+ max-height: 171px;
+ }
+ #confirmation-prompt {
+ color: @light-gray;
+ font-size: 16px;
+ }
.top-bar {
- height: 32px;
+ margin-top: 8px;
margin-bottom: 0px;
- z-index: 2;
- .logo-container {
- height: inherit;
- text-align: center;
- line-height: 24px;
- .logo {
- // margin-left: 20px;
- margin-top: 5px;
- text-align: center;
- font-size: 26px;
- font-weight: 700;
- color: @white;
- letter-spacing: 0.01em;
- }
- }
+ // Prevent annoying behavior of highlighting instead of clicking
+ -webkit-user-select: none; /* Chrome all / Safari all */
+ -moz-user-select: none; /* Firefox all */
+ -ms-user-select: none;
.account-tabs {
height: inherit;
.tabs {
@@ -211,7 +207,11 @@ body {
height: inherit;
line-height: 32px;
a {
- color: @white;
+ -webkit-user-select: none; /* Chrome all / Safari all */
+ -moz-user-select: none; /* Firefox all */
+ -ms-user-select: none;
+
+ color: @light-gray;
text-transform: capitalize;
font-weight: 700;
font-size: 12px;
@@ -220,7 +220,7 @@ body {
}
}
.active {
- font-size: 14px;
+ font-size: 12px;
color: @light-purple;
}
i {
@@ -235,6 +235,7 @@ body {
color: @white;
font-size: 10px;
line-height: 24px;
+ height: 24px;
padding: 0 .5rem;
&:hover {
background-color: lighten(@light-purple, 5%);
@@ -246,15 +247,16 @@ body {
font-size: 16px;
}
}
-
}
.indicator {
background-color: @light-purple;
+ margin: 0 16px 0 16px;
height: 2px;
}
}
}
+
}
.content {
diff --git a/webapp/static/less/base.less b/project/webapp/static/less/base.less
similarity index 98%
rename from webapp/static/less/base.less
rename to project/webapp/static/less/base.less
index 86b343b94..17e12232b 100644
--- a/webapp/static/less/base.less
+++ b/project/webapp/static/less/base.less
@@ -23,7 +23,9 @@ html {
font-family: 'Lato', sans-serif;
color: @text;
}
-
+body {
+ -webkit-font-smoothing: antialiased;
+}
// Body Text
.body-lato {
font-family: 'Lato', sans-serif;
diff --git a/webapp/static/less/beta.less b/project/webapp/static/less/beta.less
similarity index 100%
rename from webapp/static/less/beta.less
rename to project/webapp/static/less/beta.less
diff --git a/project/webapp/static/less/components/map.less b/project/webapp/static/less/components/map.less
new file mode 100644
index 000000000..2d542952f
--- /dev/null
+++ b/project/webapp/static/less/components/map.less
@@ -0,0 +1,50 @@
+#location {
+ .input-field, #autocomplete {
+ @media only screen and (max-width: 600px) {
+ width: 200px;
+ }
+ width: 350px;
+ }
+
+ .map-wrapper {
+ .map-overlay {
+ position: relative;
+ .progress {
+ position: absolute;
+ left: 0;
+ right: 0;
+ margin: 0 auto 0 auto;
+ width: 350px;
+ @media only screen and (max-width: 600px) {
+ width: 200px;
+ }
+ z-index: 2;
+
+ }
+ }
+ #map {
+ width: 350px;
+ height: 250px;
+ @media only screen and (max-width: 600px) {
+ width: 200px;
+ height: 200px;
+ }
+ margin-left: auto;
+ margin-right: auto;
+ border: solid 1px @dark-white;
+ z-index: 1;
+ }
+ }
+ .rep-title{
+ margin-top: 20px;
+ }
+ #rep-text {
+ margin: 20px 0 20px 0;
+ }
+ #rep-list {
+ margin-top: 15px;
+ .rep {
+ margin: 5px;
+ }
+ }
+}
diff --git a/project/webapp/static/less/components/navbar.less b/project/webapp/static/less/components/navbar.less
new file mode 100644
index 000000000..619167296
--- /dev/null
+++ b/project/webapp/static/less/components/navbar.less
@@ -0,0 +1,89 @@
+.nav {
+ height: 40px;
+ line-height: 40px;
+ background-color: @dark-purple;
+
+ .nav-wrapper {
+ margin-left: 8px;
+ margin-right: 8px;
+
+ .nav-item {
+ line-height: inherit;
+ height: 40px;
+ display: inline-flex;
+ }
+ .logo {
+ height: inherit;
+ text-align: center;
+ line-height: inherit;
+ .wordmark {
+ text-align: center;
+ font-size: 24px;
+ font-weight: 700;
+ color: @white;
+ letter-spacing: 0.01em;
+ padding-bottom: 8px;
+
+ }
+ }
+ .nav-menu {
+ height: 40px;
+ line-height: 40px;
+ padding-top: 4px;
+
+ .nav-menu-item,.nav-menu-item * {
+ display: inline-block;
+ height: 32px;
+ line-height: 32px;
+ }
+ .button-icon i {
+ color: @white;
+ font-size: 24px;
+ vertical-align: super;
+ }
+ .button-icon i:hover {
+ color: @blue;
+ cursor: pointer;
+ }
+ }
+ .chip-menu-profile {
+ margin-bottom: 0;
+ vertical-align: top;
+ // Prevent annoying behavior of highlighting instead of clicking
+ -webkit-user-select: none; /* Chrome all / Safari all */
+ -moz-user-select: none; /* Firefox all */
+ -ms-user-select: none;
+ display: inline-block;
+ margin-left: 8px;
+ margin-right: 4px;
+
+ .label-username {
+ vertical-align: top;
+ line-height: 32px;
+ font-size: 16px;
+ padding-right: 4px;
+ color: @white
+ }
+ .chip-image-profile {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ border-radius: 50%;
+ }
+ .button-icon-toggle {
+ display: inline-block;
+ i {
+ color: @white
+ }
+ }
+ &:hover {
+ .label-username, i{
+ color: @blue;
+ }
+ cursor: pointer;
+ }
+
+ }
+
+ }
+}
diff --git a/webapp/static/less/feed.less b/project/webapp/static/less/feed.less
similarity index 98%
rename from webapp/static/less/feed.less
rename to project/webapp/static/less/feed.less
index 11e18d535..4e809b315 100644
--- a/webapp/static/less/feed.less
+++ b/project/webapp/static/less/feed.less
@@ -74,8 +74,8 @@ body {
}
}
- nav {
- background-color: @dark-purple;
+ .feed-nav {
+ background-color: @white;
height: 48px;
line-height: 48px;
.nav-wrapper {
@@ -97,7 +97,7 @@ body {
.category-item {
height: 48px;
line-height: 48px;
- color: @white;
+ color: @light-gray;
font-size: 14px;
i {
line-height: inherit;
@@ -108,7 +108,6 @@ body {
}
&.active {
color: @light-purple;
- font-weight: 600;
}
}
.dropdown-wrapper {
diff --git a/webapp/static/less/how_it_works.less b/project/webapp/static/less/how_it_works.less
similarity index 100%
rename from webapp/static/less/how_it_works.less
rename to project/webapp/static/less/how_it_works.less
diff --git a/webapp/static/less/landing.less b/project/webapp/static/less/landing.less
similarity index 100%
rename from webapp/static/less/landing.less
rename to project/webapp/static/less/landing.less
diff --git a/webapp/static/less/login.less b/project/webapp/static/less/login.less
similarity index 100%
rename from webapp/static/less/login.less
rename to project/webapp/static/less/login.less
diff --git a/webapp/static/less/setup.less b/project/webapp/static/less/setup.less
similarity index 100%
rename from webapp/static/less/setup.less
rename to project/webapp/static/less/setup.less
diff --git a/webapp/static/less/static_common.less b/project/webapp/static/less/static_common.less
similarity index 100%
rename from webapp/static/less/static_common.less
rename to project/webapp/static/less/static_common.less
diff --git a/webapp/static/less/supportus.less b/project/webapp/static/less/supportus.less
similarity index 100%
rename from webapp/static/less/supportus.less
rename to project/webapp/static/less/supportus.less
diff --git a/webapp/static/less/thread.less b/project/webapp/static/less/thread.less
similarity index 71%
rename from webapp/static/less/thread.less
rename to project/webapp/static/less/thread.less
index 511b73681..d8b7703dc 100644
--- a/webapp/static/less/thread.less
+++ b/project/webapp/static/less/thread.less
@@ -23,23 +23,139 @@
body {
- -webkit-font-smoothing: antialiased;
- ::-webkit-scrollbar {
- width: 8px;
- }
- ::-webkit-scrollbar-track {
- background-color: @dark-white;
- }
- ::-webkit-scrollbar-thumb {
- background-color: @light-gray;
- margin-top: 4px;
- margin-bottom: 4px;
- }
+ // -webkit-font-smoothing: antialiased;
+ // ::-webkit-scrollbar {
+ // width: 8px;
+ // }
+ // ::-webkit-scrollbar-track {
+ // background-color: @dark-white;
+ // }
+ // ::-webkit-scrollbar-thumb {
+ // background-color: @light-gray;
+ // margin-top: 4px;
+ // margin-bottom: 4px;
+ // }
}
.linked {
border: 1px solid #501cb5;
}
+
+
#thread {
+ .thread-nav {
+ background-color: transparent;
+ border-bottom: 1px solid @dark-white;
+ box-shadow: none;
+ color: @black;
+ height: 56px;
+ // line-height: 40px;
+ display: inline-block;
+
+ .nav-wrapper {
+ .nav-item {
+
+ padding-left: 0;
+ padding-right: 0;
+ &.nav-categories {
+ max-width: 1200px !important;
+ margin-left: auto !important;
+ margin-right: auto !important;
+
+ padding-left: 16px;
+ padding-right: 16px;
+ }
+
+
+ }
+
+ #thread-meta {
+ display: flex;
+ padding-top: 8px;
+ .meta-item {
+ height: 16px;
+ line-height: 16px;
+ color: @dark-gray;
+ font-size: 14px;
+
+ padding-right: 4px;
+ i {
+ vertical-align: text-bottom;
+ line-height: 16px;
+ height: 16px;
+ display: inline-block;
+
+ font-size: 1.5em;
+
+ color: @light-gray;
+ &:hover {
+ cursor: pointer;
+ color: @light-purple;
+ }
+ }
+
+ }
+
+ .title-item {
+ width: 68%;
+ }
+ .dropdown-wrapper {
+ .dropdown-content {
+ li {
+ min-height: 0;
+ &:hover {
+ background-color: white;
+ }
+ }
+
+ .meta-item {
+ color: @gray;
+ height: inherit;
+ line-height: 14px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ margin: 0;
+ &:hover {
+ color: @light-purple;
+ background-color: white;
+ }
+ &.active {
+ color: @light-purple;
+ font-weight: 600;
+ }
+ }
+ }
+ }
+ }
+ .add-button {
+ height: 32px;
+ line-height: 32px;
+
+ float: right;
+
+
+ margin-left: auto;
+ .btn{
+ font-size: 12px;
+ padding: 0 8px;
+ margin-top: 4px;
+ height: 32px;
+
+ .text {
+ padding-bottom: 4px;
+ }
+ i{
+ margin-left: 4px;
+ margin-right: 4px;
+ line-height: 32px;
+
+ font-size: 1.7em;
+
+ }
+ }
+ }
+ }
+ }
+
.edit-thread-modal {
.action-button {
display: inline-block;
@@ -96,6 +212,11 @@ body {
}
.thread-wiki {
+ max-width: 1200px !important;
+ margin-left: auto !important;
+ margin-right: auto !important;
+
+
.wiki-banner {
min-height: 30vh;
padding-bottom: 16px;
@@ -106,7 +227,7 @@ body {
.wiki-title {
padding-top: 80px;
line-height: 24px;
- font-size: 24px;
+ font-size: 16px;
word-wrap: break-word;
}
@@ -278,23 +399,32 @@ body {
}
.thread-body {
+ max-width: 1200px !important;
+ margin-left: auto !important;
+ margin-right: auto !important;
+
.btn {
background-color: @purple;
}
.body-banner {
- background-color: @dark-purple;
- box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.4);
- position: relative;
- z-index: 2;
+ display: inline-block;
+
+ // box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.4);
+ // position: relative;
+ // z-index: 1;
.category-topic {
- margin-top: 4px;
- font-size: 13px;
+ display: inline-block;
+ font-size: 16px;
+ line-height: 36px;
+ height:36px;
i {
+ display: inline-block;
position: relative;
- top: 6px;
+ line-height: 36px;
+ height:36px;
}
}
@@ -328,8 +458,10 @@ body {
// box-shadow: 0 5px 6px -3px rgba(0, 0, 0, 0.4);
position: relative;
z-index: 2;
- background-color: @barely-not-white;
- // border-right: 1px solid @dark-white;
+ background-color: @white;
+
+
+ // border-bottom: 1px solid @dark-white;
width: 25%;
margin-left: auto;
@@ -343,7 +475,7 @@ body {
padding: 6px 0.75rem 16px 0.75rem;
.enter-wiki {
- font-size: 13px;
+ font-size: 16px;
font-weight: 600;
cursor: pointer;
color: @blue;
@@ -379,9 +511,21 @@ body {
margin-top: 4px;
}
-
+ .label-other {
+ margin-right: 6px;
+ }
+ .label-recommended {
+ margin-left: 10px;
+ }
.lever {
- margin: 0 8px;
+ margin: 0;
+ width: 32px;
+ &:after {
+ width: 18px;
+ height: 18px;
+ border-radius: 18px;
+ top: -2px;
+ }
}
.badge-other.current{
background-color: @red !important;
@@ -472,7 +616,9 @@ body {
// z-index: 1;
overflow-y: scroll;
overflow-x: hidden;
- padding: 15px 32px 0 32px;
+ padding: 15px 16px 0 16px;
+ border-left: 1px solid @dark-white;
+ border-right: 1px solid @dark-white;
width: 40%;
margin-left: auto;
@@ -487,9 +633,13 @@ body {
right: auto;
// z-index: 1;
overflow-y: scroll;
+ overflow-x: hidden;
padding: 15px 15px 0 15px;
// border-left: 1px solid #ddd;
// box-shadow: 0 2px 5px @box-shadow-gray;
+ .scroll-padding {
+ width: 100%;
+ }
}
}
diff --git a/webapp/static/less/utils.less b/project/webapp/static/less/utils.less
similarity index 70%
rename from webapp/static/less/utils.less
rename to project/webapp/static/less/utils.less
index 0c34ab08e..1d9bc3a38 100644
--- a/webapp/static/less/utils.less
+++ b/project/webapp/static/less/utils.less
@@ -1,4 +1,6 @@
@import 'base';
+@import 'components/navbar';
+@import 'components/map';
.nav-button-outer {
position: fixed;
@@ -37,16 +39,16 @@
}
#live_notify_badge {
z-index: 3;
- line-height: 20px;
+ line-height: 16px;
font-size: 12px;
font-weight: 600;
text-align: center;
position: absolute;
- background-color: @blue;
+ background-color: @dark-blue;
color: @white;
- width: 20px;
- height: 20px;
- border-radius: 10px;
+ width: 16px;
+ height: 16px;
+ border-radius: 8px;
}
.civi-card, .new-civi-card {
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
@@ -113,7 +115,6 @@
i {
cursor: pointer;
- margin-left: 6px;
vertical-align: middle;
}
}
@@ -167,7 +168,7 @@
.civi-body {
padding: 0 20px;
border-bottom: 1px solid @dark-white;
- font-size: 14px;
+ font-size: 12px;
line-height: 22px;
.civi-body-inner {
@@ -328,130 +329,42 @@
}
}
-.global-nav {
- /* Component styles - this is what you need to grab */
- .absolute-center {
+@box-shadow-z1: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
+
+.dropdown-menu-wrapper{
+ display: block;
+ margin-top: -12px;
+
+ .dropdown-menu {
position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- margin: auto;
- }
- .floaty {
- position: fixed;
- bottom: 15px;
+ // top: 100%;
+
right: 0;
- z-index: 100;
- // Prevent annoying behavior of highlighting instead of clicking
- -webkit-user-select: none; /* Chrome all / Safari all */
- -moz-user-select: none; /* Firefox all */
- -ms-user-select: none;
- }
- .floaty-list {
- margin: 8px 0 0 0;
- padding: 0;
- list-style: none;
- display: inline-block;
- opacity: 0;
- transition: opacity .2s ease-out;
- }
- .floaty:hover .floaty-list {
- opacity: 1;
- }
- .floaty-list-item {
- position: relative;
- width: 40px;
- height: 40px;
- margin: 0 8px;
- display: inline-block;
- cursor: pointer;
- background-color: #f0f0f0;
- border-radius: 50%;
- box-shadow: 0 4px 8px rgba(0,0,0,.25);
- }
- .floaty-list-item > img {
- border-radius: 50%;
- }
- .floaty-list-item--yellow {
- background-color: @dark-yellow;
- }
- .floaty-list-item--yellow > svg {
- fill: #fff;
- }
- .floaty-list-item--blue {
- background-color: @dark-blue;
- }
- .floaty-list-item--blue > svg {
- fill: #fff;
- }
- .floaty-list-item-label {
- position: absolute;
- left: 50%;
- bottom: 46px;
- transform: translate(-50%, 0);
- padding: 4px 8px;
- font-size: 14px;
- color: #fff;
- background-color: #424242;
- border-radius: 2px;
- opacity: 0;
- white-space: nowrap;
- pointer-events: none;
- transition: opacity .2s ease-out;
- }
- .floaty-list-item:hover > .floaty-list-item-label {
- opacity: 1;
- }
- .floaty-btn {
- position: relative;
- display: inline-block;
- width: 56px;
- height: 56px;
- float: right;
- margin: 0 15px 0 8px;
- cursor: pointer;
- background-color: @purple;
- border-radius: 50%;
- box-shadow: 0 4px 8px rgba(0,0,0,.25);
- }
- .floaty-btn:hover .floaty-btn-label {
- opacity: 1;
- }
- .floaty-btn-label {
- position: absolute;
- top: 14px;
- right: 115%;
- padding: 4px 8px;
- font-size: 14px;
- color: #fff;
- background-color: #424242;
- border-radius: 2px;
- opacity: 0;
- pointer-events: none;
- white-space: nowrap;
- transition: opacity .2s ease-out;
- }
- .floaty-btn-icon {
- transition: all .2s;
- }
- .floaty-btn-icon-plus {
- -webkit-transform: rotate(-45deg);
- transform: rotate(-45deg);
- }
- .floaty.is-active .floaty-btn-icon-plus {
- opacity: 0;
- -webkit-transform: rotate(-130deg);
- transform: rotate(-130deg);
- }
- .floaty-btn-icon-create {
- opacity: 0;
- -webkit-transform: rotate(60deg);
- transform: rotate(60deg);
+ left: auto;
+
+ z-index: 2;
+ width: 120px;
+ padding-bottom: 4px;
+ background-color: @white;
+ border-radius: 4px;
+ box-shadow: @box-shadow-z1;
+ .dropdown-item {
+ display: block;
+ padding: 4px 10px 4px 15px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ line-height: 32px;
+ height: 32px;
+ overflow: hidden;
+ color: #24292e;
+ white-space: nowrap;
+ }
}
- .floaty.is-active .floaty-btn-icon-create {
- opacity: 1;
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
+}
+
+.location-info {
+ i {
+ color: @light-purple;
+ vertical-align: bottom;
}
}
diff --git a/webapp/templates/account.html b/project/webapp/templates/account.html
similarity index 87%
rename from webapp/templates/account.html
rename to project/webapp/templates/account.html
index 80262770d..257d82a2e 100644
--- a/webapp/templates/account.html
+++ b/project/webapp/templates/account.html
@@ -78,11 +78,11 @@
+ {% include "partials/utils/global_nav.html" %}
+
+
-
+
diff --git a/webapp/templates/general-message.html b/project/webapp/templates/general-message.html
similarity index 100%
rename from webapp/templates/general-message.html
rename to project/webapp/templates/general-message.html
diff --git a/webapp/templates/invite.html b/project/webapp/templates/invite.html
similarity index 96%
rename from webapp/templates/invite.html
rename to project/webapp/templates/invite.html
index 053ae0c71..0457facfe 100644
--- a/webapp/templates/invite.html
+++ b/project/webapp/templates/invite.html
@@ -36,9 +36,7 @@
+
+
+
+
+{% register_notify_callbacks fetch=99 callbacks='fill_notification_list,fill_notification_badge,render_notifications' %}
+
+
diff --git a/webapp/templates/partials/utils/profile_button.html b/project/webapp/templates/partials/utils/profile_button.html
similarity index 100%
rename from webapp/templates/partials/utils/profile_button.html
rename to project/webapp/templates/partials/utils/profile_button.html
diff --git a/webapp/templates/partials/utils/response.html b/project/webapp/templates/partials/utils/response.html
similarity index 100%
rename from webapp/templates/partials/utils/response.html
rename to project/webapp/templates/partials/utils/response.html
diff --git a/webapp/templates/static_templates/about.html b/project/webapp/templates/static_templates/about.html
similarity index 100%
rename from webapp/templates/static_templates/about.html
rename to project/webapp/templates/static_templates/about.html
diff --git a/webapp/templates/static_templates/how_it_works.html b/project/webapp/templates/static_templates/how_it_works.html
similarity index 100%
rename from webapp/templates/static_templates/how_it_works.html
rename to project/webapp/templates/static_templates/how_it_works.html
diff --git a/webapp/templates/static_templates/landing.html b/project/webapp/templates/static_templates/landing.html
similarity index 100%
rename from webapp/templates/static_templates/landing.html
rename to project/webapp/templates/static_templates/landing.html
diff --git a/webapp/templates/static_templates/static_footer.html b/project/webapp/templates/static_templates/static_footer.html
similarity index 100%
rename from webapp/templates/static_templates/static_footer.html
rename to project/webapp/templates/static_templates/static_footer.html
diff --git a/webapp/templates/static_templates/static_nav.html b/project/webapp/templates/static_templates/static_nav.html
similarity index 100%
rename from webapp/templates/static_templates/static_nav.html
rename to project/webapp/templates/static_templates/static_nav.html
diff --git a/webapp/templates/static_templates/support_us.html b/project/webapp/templates/static_templates/support_us.html
similarity index 100%
rename from webapp/templates/static_templates/support_us.html
rename to project/webapp/templates/static_templates/support_us.html
diff --git a/webapp/templates/thread.html b/project/webapp/templates/thread.html
similarity index 95%
rename from webapp/templates/thread.html
rename to project/webapp/templates/thread.html
index 3f91d42b2..46e147fea 100644
--- a/webapp/templates/thread.html
+++ b/project/webapp/templates/thread.html
@@ -60,6 +60,7 @@
var username = '{{user.username}}',
threadId = '{{thread_id}}',
+ thread_is_draft = '{{is_draft}}',
thread_body_data = JSON.parse('{{ thread_body_data|escapejs }}'),
thread_wiki_data = JSON.parse('{{ thread_wiki_data|escapejs }}');
@@ -69,7 +70,8 @@
var threadView = new cw.ThreadView({
username: username,
model: threadModel,
- civis: civiCollection
+ civis: civiCollection,
+ is_draft: (thread_is_draft == 'True')
});
// HACK: TEMP Websocket fun
diff --git a/webapp/templates/user-setup.html b/project/webapp/templates/user-setup.html
similarity index 100%
rename from webapp/templates/user-setup.html
rename to project/webapp/templates/user-setup.html
diff --git a/webapp/templates/user/password_reset.html b/project/webapp/templates/user/password_reset.html
similarity index 100%
rename from webapp/templates/user/password_reset.html
rename to project/webapp/templates/user/password_reset.html
diff --git a/webapp/templates/user/reset_by_email.html b/project/webapp/templates/user/reset_by_email.html
similarity index 100%
rename from webapp/templates/user/reset_by_email.html
rename to project/webapp/templates/user/reset_by_email.html
diff --git a/project/webapp/templates/user/settings.html b/project/webapp/templates/user/settings.html
new file mode 100644
index 000000000..a4f2beb3e
--- /dev/null
+++ b/project/webapp/templates/user/settings.html
@@ -0,0 +1,148 @@
+
+
+